Удалить миллионы строк из таблицы SQL

9

Мне нужно удалить более 16 миллионов записей из таблицы строк с 221 миллионами, и это происходит очень медленно.

Буду признателен, если вы поделитесь предложениями, чтобы сделать код ниже быстрее:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

DECLARE @BATCHSIZE INT,
        @ITERATION INT,
        @TOTALROWS INT,
        @MSG VARCHAR(500);
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;

BEGIN TRY
    BEGIN TRANSACTION;

    WHILE @BATCHSIZE > 0
        BEGIN
            DELETE TOP (@BATCHSIZE) FROM MySourceTable
            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;
        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

План выполнения (ограничен на 2 итерации)

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

VendorIdявляется PK и некластеризованным , где кластерный индекс не используется этим сценарием. Есть 5 других неуникальных некластеризованных индексов.

Задача - «удалить поставщиков, которых нет в другой таблице», и скопировать их в другую таблицу. У меня есть 3 таблицы vendors, SpecialVendors, SpecialVendorBackups. Попытка удалить то, SpecialVendorsчто не существует в Vendorsтаблице, и создать резервную копию удаленных записей на случай, если я делаю что-то неправильно, и мне нужно вернуть их через неделю или две.

cilerler
источник
Я бы работал над оптимизацией этого запроса и попробовал бы левое соединение, где null
paparazzo

Ответы:

8

План выполнения показывает, что он читает строки из некластеризованного индекса в некотором порядке, а затем выполняет поиск для каждой внешней строки, считанной для оценки NOT EXISTS

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

Вы удаляете 7,2% таблицы. 16 000 000 строк в 3556 партиях из 4500

Предполагая, что соответствующие строки в конечном итоге распределены по всему индексу, это означает, что он будет удалять примерно 1 строку через каждые 13,8 строки.

Таким образом, итерация 1 прочитает 62 156 строк и выполнит столько операций поиска индекса, прежде чем найдет 4 500 для удаления.

итерация 2 будет читать 57 656 (62 156 - 4500) строк, которые определенно не будут удовлетворять игнорированию любых одновременных обновлений (поскольку они уже были обработаны), а затем еще 62 156 строк, чтобы получить 4500 для удаления.

итерация 3 будет читать (2 * 57 656) + 62 156 строк и так далее, пока, наконец, итерация 3556 не будет читать (3 555 * 57 656) + 62 156 строк и выполнять столько операций поиска.

Таким образом, число поисков индекса, выполненных во всех пакетах, составляет SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

Который ((3555 * 3556 / 2) * 57656) + (3556 * 62156)- или364,652,494,976

Я бы посоветовал вам сначала материализовать строки для удаления во временную таблицу.

INSERT INTO #MyTempTable
SELECT MySourceTable.PK,
       1 + ( ROW_NUMBER() OVER (ORDER BY MySourceTable.PK) / 4500 ) AS BatchNumber
FROM   MySourceTable
WHERE  NOT EXISTS (SELECT *
                   FROM   dbo.vendor AS v
                   WHERE  VendorId = v.Id) 

И измените « DELETEУдалить». WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)Возможно, вам все равно потребуется включить NOT EXISTSв сам DELETEзапрос, чтобы обслуживать обновления, поскольку временная таблица была заполнена, но это должно быть гораздо более эффективным, поскольку для этого потребуется всего 4500 запросов на пакет.

Мартин Смит
источник
Когда вы говорите «материализуйте строки, которые нужно сначала удалить во временную таблицу», вы предлагаете поместить все эти записи со всеми столбцами во временную таблицу? или только PKколонка? (Я полагаю, что вы предлагаете мне полностью переместить эти файлы в временную таблицу, но хотели перепроверить)
cilerler
@cilerler - Просто ключевой столбец (ы)
Мартин Смит
Можете ли вы быстро просмотреть это, если я правильно понял, что вы сказали, или нет, пожалуйста?
cilerler
@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTableпросто нужно DELETE FROM MySourceTable также индексировать временную таблицу CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );и VendorIdопределенно ПК сам по себе? У вас> 221 миллион разных продавцов?
Мартин Смит,
Спасибо Мартин, проверим это после 6 вечера. И ваш ответ
таков:
4

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

Джо Оббиш
источник
Спасибо за подробную информацию. Я установил этот предел 4500, чтобы не блокировать таблицу. Если я не ошибаюсь, SQL имеет жесткий предел, который блокирует всю таблицу, если число удалений превышает 5000. И поскольку это будет длительный процесс, я не могу попытаться заблокировать эту таблицу на длительный период времени. Если я установлю эти 60000 на 4500, как вы думаете, я получу такую ​​же производительность?
cilerler
@cilerler Если вы беспокоитесь об увеличении блокировки, вы можете отключить его на уровне таблицы. Нет ничего плохого в использовании пакета размером 4500. Ключ в том, что каждый цикл будет выполнять примерно одинаковое количество работы.
Джо Оббиш
Я должен принять другой ответ из-за разницы в скорости. Я протестировал ваше решение и решение @ Martin-Smith, и его версия получает больше данных ~ 2% за 10 минут теста. Ваши решения намного лучше моих, и я действительно ценю ваше время ... -
cilerler
2

На ум приходят две мысли:

Задержка, вероятно, из-за индексации с этим объемом данных. Попробуйте удалить индексы, удалить и перестроить индексы.

Или..

Может быть быстрее скопировать строки, которые вы хотите сохранить, во временную таблицу, удалить таблицу с 16 миллионами строк и переименовать временную таблицу (или скопировать в новый экземпляр исходной таблицы).

Джон
источник