Однорядная INSERT… SELECT намного медленнее, чем отдельный SELECT

18

Учитывая следующую таблицу кучи с 400 строками, пронумерованными от 1 до 400:

DROP TABLE IF EXISTS dbo.N;
GO
SELECT 
    SV.number
INTO dbo.N 
FROM master.dbo.spt_values AS SV
WHERE 
    SV.[type] = N'P'
    AND SV.number BETWEEN 1 AND 400;

и следующие настройки:

SET NOCOUNT ON;
SET STATISTICS IO, TIME OFF;
SET STATISTICS XML OFF;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

Следующее SELECTутверждение завершается примерно за 6 секунд ( демо , план ):

DECLARE @n integer = 400;

SELECT
    c = COUNT_BIG(*) 
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE 
    N.number <= @n
    AND N2.number <= @n
    AND N3.number <= @n
OPTION
    (OPTIMIZE FOR (@n = 1));

Примечание: @ Данное OPTIMIZE FORпредложение предназначено только для создания репродукции разумного размера, в которой отражены основные детали реальной проблемы, включая недооценку количества элементов, которая может возникнуть по ряду причин.

Когда однострочный вывод записывается в таблицу, это занимает 19 секунд ( демо , план ):

DECLARE @T table (c bigint NOT NULL);

DECLARE @n integer = 400;

INSERT @T
    (c)
SELECT
    c = COUNT_BIG(*) 
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE 
    N.number <= @n
    AND N2.number <= @n
    AND N3.number <= @n
OPTION
    (OPTIMIZE FOR (@n = 1));

Планы выполнения выглядят идентичными, кроме вставки одной строки.

Кажется, все дополнительное время расходуется на загрузку процессора.

Почему INSERTутверждение намного медленнее?

Пол Уайт восстановил Монику
источник

Ответы:

21

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

Недостаточная оценка количества элементов преднамеренно вводится посредством того, OPTIMIZE FORчто кучи сканируются намного чаще, чем ожидает оптимизатор, и не вводят катушку, как обычно.

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

Этот SELECTоператор выигрывает от оптимизации, которая позволяет пропускать общие блокировки на уровне строк (принимая только блокировки на уровне страниц с общими намерениями), когда нет опасности чтения незафиксированных данных и нет данных вне строки.

Оператор INSERT...SELECTне получает выгоды от этой оптимизации, поэтому миллионы блокировок RID берутся и освобождаются каждую секунду во втором случае вместе с блокировками на уровне страниц с общим намерением.

Огромное количество операций блокирования приходится на дополнительный процессор и затраченное время.

Наиболее естественный обходной путь - обеспечить оптимизатору (и механизму хранения) приемлемые оценки мощности, чтобы они могли сделать правильный выбор.

Если это не практично в реальном использовании случае, INSERTи SELECTзаявления могут быть разделены, в результате из SELECTпроводится в переменной. Это позволит SELECTоператору извлечь выгоду из оптимизации пропуска блокировки.

Изменение уровня изоляции также можно заставить работать, либо не снимая общие блокировки, либо обеспечивая быстрое наращивание блокировки.

В качестве конечной точки интереса можно SELECTзаставить запрос выполняться даже быстрее, чем в оптимизированном случае, путем принудительного использования катушек с использованием недокументированного флага трассировки 8691.

Пол Уайт восстановил Монику
источник