Почему мой запрос 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);

Msg 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. Один из способов получить обработку в пакетном режиме для таблицы хранилища строк - присоединиться к пустой CCI следующим образом:

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 изменяет предположение об уникальности функции, оно может перестать работать. У этого есть другая оценка с унаследованным CE. Также этот тип оптимизации против кучи не является хорошей идеей. Если таблица будет перестроена, это может оказаться в худшем случае, когда почти все строки должны быть прочитаны из таблицы.

Для таблицы с уникальным столбцом (такой как пример кластеризованного индекса в вопросе) у нас есть лучшие варианты. Например, мы можем обмануть оптимизатор, используя 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для уникального столбца.

Джо Оббиш
источник
12

Вы уже правильно ответили на свои вопросы.

Я просто хочу добавить замечание, что наиболее эффективный способ - это на самом деле сканировать всю таблицу - если она может быть организована как «куча» columnstore :

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, 
 lob логическое чтение 66 , lob физическое чтение 0, lob read-forward читает 0.
Таблица «X_10_DISTINCT_HEAP». Сегмент читает 13, сегмент пропущен 0.

 Время выполнения SQL Server:
   Время процессора = 0 мс, прошедшее время = 11 мс.

Hash Match (Flow Distinct) в настоящее время не может выполняться в пакетном режиме. Методы, которые используют это, намного медленнее из-за (невидимого) дорогостоящего перехода от пакетной обработки к строке. Например:

SET ROWCOUNT 10;

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

SET ROWCOUNT 0;

дает:

План выполнения Flow Distinct

Таблица «X_10_DISTINCT_HEAP». Количество сканирования 1,
 логическое чтение 0, физическое чтение 0, чтение с опережением 0, 
 lob - логическое чтение 20 , lob физическое чтение - 0, lob read-forward - 0.
Таблица «X_10_DISTINCT_HEAP». Сегмент читает 4 , сегмент пропущен 0.

 Время выполнения SQL Server:
   Время процессора = 640 мс, прошедшее время = 680 мс.

Это медленнее, чем когда таблица организована как куча хранилища строк.

Пол Уайт говорит, что GoFundMonica
источник
5

Вот попытка эмулировать повторное частичное сканирование (аналогично, но не то же самое, что и сканирование с пропуском) с использованием рекурсивного CTE. Цель - поскольку у нас нет индекса (id)- избежать сортировки и многократного сканирования таблицы.

Он делает несколько трюков, чтобы обойти некоторые рекурсивные ограничения CTE:

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

Для кучи (при условии, что столбец назван id) test-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 ;
ypercubeᵀᴹ
источник
С этим решением связана довольно тонкая проблема с производительностью. В итоге он выполняет дополнительный поиск по таблице после того, как находит N-е значение. Таким образом, если есть 10 различных значений для топ-10, он будет искать 11-е значение, которого там нет. В итоге вы получаете дополнительное полное сканирование, и 10 миллионов ROW_NUMBER () вычислений действительно складываются. У меня есть обходной путь, который ускоряет запрос в 20 раз на моей машине. Что вы думаете? brentozar.com/pastetheplan/?id=SkDhAmFKe
Джо Оббиш
2

Для полноты, другой способ решения этой проблемы - использовать OUTER APPLY . Мы можем добавить 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 логических чтений. Мы получаем новое различающееся значение каждые 100 тыс. Строк, поэтому я ожидаю, что этот запрос будет сканировать около 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 мс прошедшего времени. Определенно больше, чем я ожидал. Добавление первичного ключа в табличную переменную, кажется, помогает, хотя я не уверен, почему. Могут быть и другие возможные оптимизации.
Джо Оббиш