Чому мій запит SELECT DISTINCT TOP N сканує всю таблицю?


28

Я зіткнувся з кількома SELECT DISTINCT TOP Nзапитами, які, як видається, оптимізовані запитами SQL Server, погано оптимізовані. Почнемо з розгляду тривіального прикладу: мільйонна таблиця рядків з двома змінними значеннями. Я буду використовувати функцію GetNums для генерування даних:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

Для наступного запиту:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Server може знайти два різних значення, просто сканувавши першу сторінку даних, але замість цього вона сканує всі дані . Чому SQL Server просто не сканує, поки не знайде потрібну кількість різних значень?

Для цього питання, будь ласка, використовуйте наступні тестові дані, що містять 10 мільйонів рядків з 10 різними значеннями, згенерованими в блоках:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

Відповіді для таблиці з кластерним індексом також прийнятні:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

Наступний запит сканує всі 10 мільйонів рядків із таблиці . Як я можу отримати щось, що не сканує всю таблицю? Я використовую SQL Server 2016 SP1.

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

Відповіді:


30

Існує три різні правила оптимізатора, які можуть виконувати 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_HEAP1165 мс та виконує 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_CI333 мс та виконує 297 мс часу процесора та 3011 логічних зчитувань.

Підводячи підсумок, оптимізатор запитів припускає, що всі рядки будуть зчитуватися з таблиці для SELECT DISTINCT TOP Nзапитів, коли N> = кількість оцінених окремих рядків з таблиці. Оператор хеш-матчу (сукупності) може мати ту саму ціну, що й оператор хеш-відповідника (різний потік), але оптимізатор завжди вибирає оператора сукупності. Це може призвести до зайвих логічних зчитувань, коли достатньо чітких значень знаходяться біля початку сканування таблиці. Два способи навести оптимізатора на використання оператора хеш-відповідника (розрізнення потоку) - знизити ціль рядка за допомогою OPTIMIZE FORпідказки або збільшити орієнтовну кількість окремих рядків за допомогою LAG()або SUBSTRINGв унікальному стовпчику.


12

Ви вже правильно відповіли на власні запитання.

Я просто хочу додати зауваження, що найефективнішим способом є насправді сканування всієї таблиці - якщо вона може бути організована як стовпчик «купи» :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

Простий запит:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

потім дає:

План виконання

Таблиця "X_10_DISTINCT_HEAP". Кількість сканувань 1,
 логічне зчитування 0, фізичне зчитування 0, читання вперед-0, 
 Лобічне логічне зчитування 66 , лоб фізичне зчитування 0, лоб зчитування вперед - 0.
Таблиця "X_10_DISTINCT_HEAP". Сегмент читає 13, сегмент пропускає 0.

 Часи виконання SQL Server:
   Час процесора = 0 мс, минулий час = 11 мс.

Наразі хеш-матч (Flow Distinct) наразі не може виконуватися в пакетному режимі. Методи, які використовують це, набагато повільніше через (невидимий) дорогий перехід від пакетної до рядкової обробки. Наприклад:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

Дає:

План виконання потоку чітко

Таблиця "X_10_DISTINCT_HEAP". Кількість сканувань 1,
 логічне зчитування 0, фізичне зчитування 0, читання вперед-0, 
 Лобічне логічне зчитування 20 , лоб фізичне зчитування 0, лоб зчитування вперед - 0.
Таблиця "X_10_DISTINCT_HEAP". Сегмент читає 4 , сегмент пропускає 0.

 Часи виконання SQL Server:
   Час процесора = 640 мс, минулий час = 680 мс.

Це відбувається повільніше, ніж тоді, коли таблиця організована у вигляді купівлі рядків.


5

Ось спроба імітувати повторне часткове сканування (схоже на, але не таке, як пропускне сканування) за допомогою рекурсивного CTE. Мета - оскільки у нас немає індексу (id)- уникнути сортування та багаторазового сканування на столі.

Це робить кілька хитрощів, щоб обійти деякі рекурсивні обмеження CTE:

  • Не TOPдопускається до рекурсивної частини. Ми використовуємо підзапит і ROW_NUMBER()замість цього.
  • Ми не можемо мати кілька посилань на постійну частину або використовувати LEFT JOINабо використовувати NOT IN (SELECT id FROM cte)з рекурсивної частини. Щоб обійти, ми будуємо VARCHARрядок, який акумулює всі idзначення, подібні STRING_AGGдо ієрархіїID, а потім порівнюємо з LIKE.

Для купи (якщо колонка названа id) тест-1 на rextester.com .

Це, як показали тести, - не дозволяє уникнути багаторазового сканування, але працює в порядку, коли на перших кількох сторінках виявляються різні значення. Якщо значення не розподілені рівномірно, це може зробити багаторазове сканування на великих частинах таблиці - це, звичайно, призводить до низької продуктивності.

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

і коли таблиця кластеризована (CI увімкнено unique_key), test-2 на rextester.com .

Для цього використовується кластерний індекс ( WHERE x.unique_key > ct.unique_key), щоб уникнути багаторазового сканування:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;

У цьому рішенні є досить тонка проблема продуктивності. Це в кінцевому підсумку робить додатковий пошук на столі після того, як він знайде N-е значення. Отже, якщо для топ-10 є 10 різних значень, вона буде шукати 11-е значення, якого там немає. Ви закінчуєте додаткове повне сканування, і 10 мільйонів ROW_NUMBER () обчислень дійсно складаються. У мене є вирішення цього питання, яке прискорює запит 20X на моїй машині. Як ти гадаєш? brentozar.com/pastetheplan/?id=SkDhAmFKe
Джо Оббіш

2

Для повноти ще одним способом наближення цієї проблеми є використання ЗОВНІШНЬОЇ ЗАЯВКИ . Ми можемо додати OUTER APPLYоператора для кожного окремого значення, яке нам потрібно знайти. Це за концепцією схоже на рекурсивний підхід ypercube, але фактично рекурсія виписана вручну. Однією з переваг є те, що ми можемо використовувати TOPу похідних таблицях замість ROW_NUMBER()обходу. Одним з великих недоліків є те, що текст запиту збільшується в міру Nзбільшення.

Ось одна реалізація для запиту проти купи:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Ось фактичний план запитів для вищезазначеного запиту. На моїй машині цей запит завершується за 713 мс з 625 мс часу процесора та 12605 логічних зчитувань. Ми отримуємо нове чітке значення кожні 100k рядків, тому я б очікував, що цей запит просканує близько 900000 * 10 * 0,5 = 4500000 рядків. Теоретично цей запит повинен робити п’ять разів більше логічного читання цього запиту з іншої відповіді:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Цей запит зробив 2537 логічних прочитаних. 2537 * 5 = 12685, що досить близько до 12605.

Для таблиці з кластерним індексом ми можемо зробити краще. Це тому, що ми можемо передати останнє кластерне значення ключа у похідну таблицю, щоб уникнути сканування одних і тих же рядків двічі. Одна реалізація:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Ось фактичний план запитів для вищезазначеного запиту. На моїй машині цей запит завершується за 154 мс із 140 мс процесорного часу та 3203 логічними читаннями. Це, здавалося, працює трохи швидше, ніжOPTIMIZE FOR запит проти кластерної таблиці індексів. Я не очікував цього, тому намагався ретельніше оцінити результати. Моя методологія полягала в тому, щоб запустити кожен запит десять разів без наборів результатів і переглянути сукупні числа з sys.dm_exec_sessionsі sys.dm_exec_session_wait_stats. Сесія 56 - це APPLYзапит, а сесія 63 - OPTIMIZE FORзапит.

Вихід sys.dm_exec_sessions :

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

Здається, очевидна перевага в запиті cpu_time та elapsed_time APPLY.

Вихід sys.dm_exec_session_wait_stats :

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

OPTIMIZE FORЗапит має додатковий тип очікування, RESERVED_MEMORY_ALLOCATION_EXT . Я точно не знаю, що це означає. Це може бути просто вимірювання накладних витрат у оператора хеш-матчу (розрізнення потоку). У будь-якому випадку, можливо, не варто турбуватися про різницю в 70 мс в процесі процесора.


1

Я думаю, у вас є відповідь, чому
це може бути способом вирішити це питання
Я знаю, що це виглядає безладним, але план виконання сказав, що окремий топ-2 становив 84% від вартості

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;

Цей код зайняв 5 секунд на моїй машині. Схоже, що приєднання до змінної таблиці додають досить великі накладні витрати. В остаточному запиті змінна таблиці була відсканована 892800 разів. Цей запит зайняв 1359 мс процесорного часу та 1374 мс минулого часу. Однозначно більше, ніж я очікував. Додавання первинного ключа до змінної таблиці, здається, допомагає, хоча я не впевнений, чому. Можуть бути й інші можливі оптимізації.
Джо Оббіш
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.