План выполнения предполагает, что каждый последующий цикл будет выполнять больше работы, чем предыдущий цикл. Предполагая, что удаляемые строки равномерно распределены по всей таблице, первый цикл должен будет сканировать около 4500 * 221000000/16000000 = 62156 строк, чтобы найти 4500 строк для удаления. Он также будет выполнять такое же количество поисков кластеризованного индекса по vendor
таблице. Однако второй цикл должен будет прочитать те же строки 62156 - 4500 = 57656, которые вы не удалили в первый раз. Можно ожидать, что второй цикл будет сканировать 120000 строк MySourceTable
и выполнять 120000 операций поиска по vendor
таблице. Количество работы, необходимое для цикла, увеличивается с линейной скоростью. В качестве приближения можно сказать, что в среднем цикле нужно будет прочитать 102516868 строк из MySourceTable
и выполнить 102516868 запросов противvendor
Таблица. Чтобы удалить 16 миллионов строк с размером пакета 4500, ваш код должен выполнить 16000000/4500 = 3556 циклов, поэтому общий объем работы, выполняемой вашим кодом, составляет около 364,5 миллиарда строк, считанных из MySourceTable
и 364,5 миллиарда поисков индекса.
Меньшая проблема заключается в том, что вы используете локальную переменную @BATCHSIZE
в выражении TOP без какой- RECOMPILE
либо другой подсказки. Оптимизатор запросов не будет знать значение этой локальной переменной при создании плана. Предполагается, что он равен 100. В действительности вы удаляете 4500 строк вместо 100, и из-за этого несоответствия вы можете получить менее эффективный план. Оценка низкой мощности при вставке в таблицу также может привести к снижению производительности. SQL Server может выбрать другой внутренний API для вставки, если он считает, что ему нужно вставить 100 строк, а не 4500 строк.
Один из вариантов - просто вставить первичные ключи / кластерные ключи строк, которые вы хотите удалить, во временную таблицу. В зависимости от размера ваших ключевых столбцов это может легко поместиться в tempdb. В этом случае вы можете получить минимальное количество записей , что означает, что журнал транзакций не будет взорван. Вы также можете получить минимальное логирование для любой базы данных с моделью восстановления SIMPLE
. Смотрите ссылку для получения дополнительной информации о требованиях.
Если это не вариант, вы должны изменить свой код, чтобы использовать кластерный индекс MySourceTable
. Ключевым моментом является написание вашего кода, чтобы вы выполняли примерно одинаковое количество работы за цикл. Вы можете сделать это, воспользовавшись индексом, а не просто сканировать таблицу с самого начала каждый раз. Я написал сообщение в блоге, в котором рассматриваются различные методы зацикливания. Примеры в этом посте делают вставки в таблицу, а не удаляют, но вы должны быть в состоянии адаптировать код.
В приведенном ниже примере кода я предполагаю, что первичный ключ и ваш кластерный ключ MySourceTable
. Я написал этот код довольно быстро и не могу его протестировать:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
DECLARE @BATCHSIZE INT,
@ITERATION INT,
@TOTALROWS INT,
@MSG VARCHAR(500)
@STARTID BIGINT,
@NEXTID BIGINT;
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;
SELECT @STARTID = ID
FROM MySourceTable
ORDER BY ID
OFFSET 0 ROWS
FETCH FIRST 1 ROW ONLY;
SELECT @NEXTID = ID
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
OFFSET (60000) ROWS
FETCH FIRST 1 ROW ONLY;
BEGIN TRY
BEGIN TRANSACTION;
WHILE @STARTID IS NOT NULL
BEGIN
WITH MySourceTable_DELCTE AS (
SELECT TOP (60000) *
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
)
DELETE FROM MySourceTable_DELCTE
OUTPUT DELETED.*
INTO MyBackupTable
WHERE NOT EXISTS (
SELECT NULL AS Empty
FROM dbo.vendor AS v
WHERE VendorId = v.Id
);
SET @BATCHSIZE = @@ROWCOUNT;
SET @ITERATION = @ITERATION + 1;
SET @TOTALROWS = @TOTALROWS + @BATCHSIZE;
SET @MSG = CAST(GETDATE() AS VARCHAR) + ' Iteration: ' + CAST(@ITERATION AS VARCHAR) + ' Total deletes:' + CAST(@TOTALROWS AS VARCHAR) + ' Next Batch size:' + CAST(@BATCHSIZE AS VARCHAR);
PRINT @MSG;
COMMIT TRANSACTION;
CHECKPOINT;
SET @STARTID = @NEXTID;
SET @NEXTID = NULL;
SELECT @NEXTID = ID
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
OFFSET (60000) ROWS
FETCH FIRST 1 ROW ONLY;
END;
END TRY
BEGIN CATCH
IF @@ERROR <> 0
AND @@TRANCOUNT > 0
BEGIN
PRINT 'There is an error occured. The database update failed.';
ROLLBACK TRANSACTION;
END;
END CATCH;
GO
Ключевая часть здесь:
WITH MySourceTable_DELCTE AS (
SELECT TOP (60000) *
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
)
Каждый цикл будет читать только 60000 строк MySourceTable
. Это должно привести к среднему размеру удаления 4500 строк на транзакцию и максимальному размеру удаления 60000 строк на транзакцию. Если вы хотите быть более консервативным с меньшим размером партии, это тоже хорошо. Эти @STARTID
переменные успехи после каждого цикла , так что вы можете избежать чтения и ту же строку более чем один раз из исходной таблицы.