Похоже, есть три разных правила оптимизатора, которые могут выполнять 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
для уникального столбца.