Існує три різні правила оптимізатора, які можуть виконувати DISTINCT
операцію у вищезазначеному запиті. Наступний запит видає помилку, яка говорить про те, що список є вичерпним:
SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);
Повідомлення 8622, рівень 16, стан 1, рядок 1
Процесор запиту не зміг скласти план запиту через підказки, визначені в цьому запиті. Повторно надішліть запит, не вказуючи жодних підказок і не використовуючи SET FORCEPLAN.
GbAggToSort
реалізує груповий сукупність (виразний) як окремий вид. Це оператор блокування, який прочитає всі дані з вхідних даних, перш ніж створювати будь-які рядки. GbAggToStrm
реалізує агрегат групи за допомогою сукупності потоків (що також вимагає введення сортування в цьому випадку). Це також оператор блокування. GbAggToHS
реалізує як хеш-матч, який ми бачили в поганому плані з питання, але він може бути реалізований як хеш-збіг (сукупність) або хеш-матч (потоку різний).
Оператор хеш-відповідника ( розрізнення потоку ) є одним із способів вирішити цю проблему, оскільки він не блокує. SQL Server повинен бути в змозі зупинити сканування, коли він знайде достатньо чітких значень.
Логічний оператор Flow Distinct сканує вхід, видаляючи дублікати. Якщо оператор Distinct споживає весь вхід перед тим, як виробляти будь-який вихід, оператор Flow Distinct повертає кожен рядок у міру отримання з вводу даних (якщо цей рядок не є дублікатом; у цьому випадку він відкидається).
Чому запит у запитанні використовує хеш-відповідність (сукупність) замість хеш-відповідності (розрізнення потоку)? Зі зміною кількості різних значень у таблиці я б очікував, що вартість запиту хеш-відповідності (розрізнення потоку) зменшиться, оскільки оцінка кількості рядків, необхідних для сканування до таблиці, повинна зменшуватися. Я б очікував, що вартість плану хеш-матчу (сукупного) збільшиться, оскільки хеш-таблиця, яку він повинен створити, збільшиться. Одним із способів дослідження цього є створення довідника щодо плану . Якщо я створю дві копії даних, але застосовую керівництво плану до однієї з них, я повинен мати можливість порівнювати хеш-збіг (сукупність) з хеш-матчем (окремим) поряд з тими ж даними. Зауважте, що я не можу цього зробити, відключивши правила оптимізатора запитів, оскільки те саме правило стосується обох планів ( GbAggToHS
).
Ось один із способів отримати керівництво щодо плану, яке я виконую:
DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;
CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);
INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);
UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;
-- run this query
SELECT DISTINCT TOP 10 VAL FROM X_PLAN_GUIDE_TARGET OPTION (MAXDOP 1)
Отримайте ручку плану та скористайтеся нею для створення посібника щодо плану:
-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM
sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;
EXEC sp_create_plan_guide_from_handle
'EVIL_PLAN_GUIDE',
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;
Посібники планують працювати лише над точним текстом запиту, тому скопіюємо його назад із посібника з плану:
SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';
Скидання даних:
TRUNCATE TABLE X_PLAN_GUIDE_TARGET;
INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);
Отримайте план запиту із застосованим посібником щодо плану:
SELECT DISTINCT TOP 10 VAL FROM X_PLAN_GUIDE_TARGET OPTION (MAXDOP 1)
Це оператор хеш-відповідності (розрізнення потоку), якого ми хотіли за допомогою наших тестових даних. Зауважте, що SQL Server розраховує прочитати всі рядки з таблиці і що орієнтовна вартість точно така ж, як і для плану з відповідним хешем (сукупністю). Тестування, яке я зробив, припустило, що витрати на два плани однакові, коли ціль рядка для плану більша або дорівнює кількості різних значень, які очікує SQL Server з таблиці, що в цьому випадку може бути просто отримано з статистика. На жаль (для нашого запиту) оптимізатор вибирає хеш-збігу (сукупність) за хеш-матч (різний потік), коли витрати однакові. Таким чином, ми знаходимося на 0,0000001 одиницях оптичного оптимізатора від плану, який ми хочемо.
Один із способів атаки на цю проблему - зменшення мети рядка. Якщо мета рядка з точки зору оптимізатора менша за кількість чітких рядків, ми, ймовірно, отримаємо хеш-збіг (розрізнення потоку). Це можна досягти за допомогою OPTIMIZE FOR
підказки:
DECLARE @j INT = 10;
SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));
Для цього запиту оптимізатор створює план так, ніби запиту потрібен лише перший рядок, але коли виконується запит, він повертає перші 10 рядків. На моїй машині цей запит сканує 892800 рядків з X_10_DISTINCT_HEAP
та завершує за 299 мс з 250 мс часу процесора та 2537 логічних зчитувань.
Зауважте, що ця методика не працюватиме, якщо статистика подає лише одне чітке значення, що може статися для вибіркової статистики щодо перекошених даних. Однак у такому випадку навряд чи ваші дані будуть упаковані досить щільно, щоб виправдати використання таких методів. Ви можете не втратити багато, скануючи всі дані в таблиці, особливо якщо це можна зробити паралельно.
Ще один спосіб атаки на цю проблему - завищення кількості оцінених різних значень, які SQL Server очікує отримати з базової таблиці. Це було складніше, ніж очікувалося. Застосування детермінованої функції, можливо, не може збільшити кількість результатів. Якщо оптимізатору запитів відомо про той математичний факт (деякі тестування припускають, що це принаймні для наших цілей), то застосування детермінованих функцій (що включає всі функції рядків ) не збільшить орієнтовну кількість окремих рядків.
Багато з недетермінованих функцій також не працювали, включаючи очевидний вибір NEWID()
та RAND()
. Однак LAG()
чи є хитрість у цьому запиті. Оптимізатор запитів очікує 10 мільйонів чітких значень проти LAG
виразу, який буде заохочувати план хеш-відповідності (розрізнення потоку) :
SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);
На моїй машині цей запит сканує 892800 рядків з X_10_DISTINCT_HEAP
1165 мс та виконує 1109 мс процесорного часу та 2537 логічних зчитувань, тож LAG()
додає зовсім небагато відносних витрат. @Paul White запропонував спробувати обробку пакетного режиму для цього запиту. На SQL Server 2016 ми можемо отримати пакетний режим обробки навіть з MAXDOP 1
. Один із способів отримати пакетну обробку таблиці таблиць рядків - це приєднання до порожнього ІСН наступним чином:
CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);
CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;
SELECT DISTINCT TOP 10 VAL
FROM
(
SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
FROM X_10_DISTINCT_HEAP
LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);
Цей код призводить до цього плану запитів .
Пол зазначив, що мені довелося змінити запит, який потрібно використовувати, LAG(..., 1)
оскільки LAG(..., 0)
він не відповідає вимогам оптимізації віконної сукупності. Ця зміна скоротила минулий час до 520 мс, а час процесора - до 454 мс.
Зауважте, що LAG()
підхід не є найбільш стабільним. Якщо Microsoft змінить припущення про унікальність щодо функції, вона може більше не працювати. Він має іншу оцінку зі спадщиною СЕ. Крім того, такий тип оптимізації щодо купи не потрібна хороша ідея. Якщо таблиця буде перебудована, можна опинитися в гіршому випадку, коли майже всі рядки потрібно прочитати з таблиці.
Проти таблиці з унікальним стовпцем (наприклад, кластерний приклад індексу у питанні) ми маємо кращі варіанти. Наприклад, ми можемо обдурити оптимізатор, використовуючи SUBSTRING
вираз, який завжди повертає порожній рядок. SQL Server не думає, що SUBSTRING
воля змінить кількість різних значень, тому якщо ми застосуємо його до унікального стовпчика, наприклад, PK, то орієнтовна кількість окремих рядків становить 10 мільйонів. Цей наступний запит отримує оператор хеш-відповідності (розрізнення потоку):
SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);
На моїй машині цей запит сканує 900000 рядків з X_10_DISTINCT_CI
333 мс та виконує 297 мс часу процесора та 3011 логічних зчитувань.
Підводячи підсумок, оптимізатор запитів припускає, що всі рядки будуть зчитуватися з таблиці для SELECT DISTINCT TOP N
запитів, коли N
> = кількість оцінених окремих рядків з таблиці. Оператор хеш-матчу (сукупності) може мати ту саму ціну, що й оператор хеш-відповідника (різний потік), але оптимізатор завжди вибирає оператора сукупності. Це може призвести до зайвих логічних зчитувань, коли достатньо чітких значень знаходяться біля початку сканування таблиці. Два способи навести оптимізатора на використання оператора хеш-відповідника (розрізнення потоку) - знизити ціль рядка за допомогою OPTIMIZE FOR
підказки або збільшити орієнтовну кількість окремих рядків за допомогою LAG()
або SUBSTRING
в унікальному стовпчику.