Почему временная таблица является более эффективным решением проблемы Хэллоуина, чем энергичная шпуля?

14

Рассмотрим следующий запрос, который вставляет строки из исходной таблицы, только если их еще нет в целевой таблице:

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);

Одна возможная форма плана включает соединение слиянием и готовую катушку. Стремительный оператор катушки присутствует, чтобы решить проблему Хэллоуина :

первый план

На моей машине приведенный выше код выполняется примерно за 6900 мс. Код Repro для создания таблиц включен в нижней части вопроса. Если я недоволен производительностью, я могу попытаться загрузить строки для вставки во временную таблицу вместо того, чтобы полагаться на готовую катушку. Вот одна из возможных реализаций:

DROP TABLE IF EXISTS #CONSULTANT_RECOMMENDED_TEMP_TABLE;
CREATE TABLE #CONSULTANT_RECOMMENDED_TEMP_TABLE (
    ID BIGINT,
    PRIMARY KEY (ID)
);

INSERT INTO #CONSULTANT_RECOMMENDED_TEMP_TABLE WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1);

Новый код выполняется примерно за 4400 мс. Я могу получить реальные планы и использовать Actual Time Statistics ™, чтобы проверить, где время тратится на уровне оператора. Обратите внимание, что запрос фактического плана добавляет значительные накладные расходы для этих запросов, поэтому итоговые данные не будут соответствовать предыдущим результатам.

╔═════════════╦═════════════╦══════════════╗
  operator    first query  second query 
╠═════════════╬═════════════╬══════════════╣
 big scan     1771         1744         
 little scan  163          166          
 sort         531          530          
 merge join   709          669          
 spool        3202         N/A          
 temp insert  N/A          422          
 temp scan    N/A          187          
 insert       3122         1545         
╚═════════════╩═════════════╩══════════════╝

План запроса с активной буферизацией, по-видимому, тратит значительно больше времени на операторы вставки и буферизации по сравнению с планом, использующим временную таблицу.

Почему план с временной таблицей более эффективен? Разве нетерпеливая шпуля в любом случае не является просто внутренней временной таблицей? Я считаю, что я ищу ответы, которые сосредоточены на внутренних органах. Я могу видеть, как стеки вызовов отличаются, но не могу понять общую картину.

Я нахожусь на SQL Server 2017 CU 11 в случае, если кто-то хочет знать. Вот код для заполнения таблиц, используемых в вышеуказанных запросах:

DROP TABLE IF EXISTS dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR;

CREATE TABLE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR (
ID BIGINT NOT NULL,
PRIMARY KEY (ID)
);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (20000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS dbo.A_HEAP_OF_MOSTLY_NEW_ROWS;

CREATE TABLE dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (
ID BIGINT NOT NULL
);

INSERT INTO dbo.A_HEAP_OF_MOSTLY_NEW_ROWS WITH (TABLOCK)
SELECT TOP (1900000) 19999999 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
Джо Оббиш
источник

Ответы:

14

Это то, что я называю « Ручная защита от Хэллоуина» .

Пример его использования с оператором обновления можно найти в моей статье « Оптимизация запросов на обновление» . Нужно быть немного осторожнее, чтобы сохранить ту же семантику, например, путем блокировки целевой таблицы от всех одновременных модификаций во время выполнения отдельных запросов, если это уместно в вашем сценарии.

Почему план с временной таблицей более эффективен? Разве нетерпеливая шпуля в любом случае не является просто внутренней временной таблицей?

Спул имеет некоторые характеристики временной таблицы, но они не являются точными эквивалентами. В частности, катушка является по существу построчной неупорядоченной вставкой в ​​структуру b-дерева . Он выигрывает от оптимизации блокировок и журналирования, но не поддерживает оптимизацию массовой загрузки .

Следовательно, часто можно повысить производительность, разделив запрос естественным образом: загрузив новые строки во временную таблицу или переменную, а затем выполняя оптимизированную вставку (без явной защиты от Хэллоуина) из временного объекта.

Такое разделение также дает вам дополнительную свободу отдельно настраивать части для чтения и записи исходного утверждения.

В качестве примечания, интересно подумать о том, как можно решить проблему Хэллоуина, используя строчные версии. Возможно, будущая версия SQL Server предоставит эту функцию в подходящих обстоятельствах.


Как упоминал Майкл Куц в комментарии, вы также можете изучить возможность использования оптимизации заполнения отверстий, чтобы избежать явного HP. Один из способов добиться этого для демонстрации - создать уникальный IDстолбец (кластеризованный, если хотите) в столбце A_HEAP_OF_MOSTLY_NEW_ROWS.

CREATE UNIQUE INDEX i ON dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (ID);

С этой гарантией оптимизатор может использовать заполнение отверстий и совместное использование набора строк:

MERGE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (SERIALIZABLE) AS HICETY
USING dbo.A_HEAP_OF_MOSTLY_NEW_ROWS AS AHOMNR
    ON AHOMNR.ID = HICETY.ID
WHEN NOT MATCHED BY TARGET
THEN INSERT (ID) VALUES (AHOMNR.ID);

План MERGE

Хотя это интересно, вы все равно сможете достичь лучшей производительности во многих случаях, используя тщательно реализованную Защиту от Хэллоуина.

Пол Уайт 9
источник
5

Чтобы немного расширить ответ Пола, часть различий в затраченном времени между подходами «катушка» и «временная таблица», по-видимому, сводится к отсутствию поддержки DML Request Sortопции в плане катушки. С недокументированным флагом трассировки 8795 истекшее время для подхода временной таблицы скачет с 4400 мс до 5600 мс.

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1, QUERYTRACEON 8795);

Обратите внимание, что это не совсем эквивалентно вставке, выполняемой планом спула. Этот запрос записывает значительно больше данных в журнал транзакций.

Тот же эффект можно увидеть наоборот с некоторой хитростью. Можно поощрять SQL Server использовать сортировку вместо спулинга для Halloween Protection. Одна реализация:

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (987654321) 
maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
ORDER BY maybe_new_rows.ID, maybe_new_rows.ID + 1
OPTION (MAXDOP 1, QUERYTRACEON 7470, MERGE JOIN);

Теперь в плане вместо катушки используется оператор TOP N Sort. Сортировка является блокирующим оператором, поэтому спул больше не нужен:

введите описание изображения здесь

Что еще более важно, у нас теперь есть поддержка этого DML Request Sortварианта. Если снова посмотреть статистику фактического времени, оператор вставки теперь занимает всего 1623 мс. Весь план занимает около 5400 мс, чтобы выполнить без запроса фактического плана.

Как объясняет Хьюго , оператор Eager Spool сохраняет порядок. Это легче всего увидеть с TOP PERCENTпланом. К сожалению, исходный запрос со спулом не может лучше использовать отсортированный характер данных в спуле.

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