Видаліть мільйони рядків із таблиці 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є ПК та некластеризований , де кластерний індекс не використовується цим сценарієм. Є ще 5 інших не унікальних, некластеризованих індексів.

Завдання полягає в тому, щоб "видалити постачальників, яких немає в іншій таблиці", і створити їх назад в іншу таблицю. У мене є 3 таблиці, vendors, SpecialVendors, SpecialVendorBackups. Намагаюся видалити ті, SpecialVendorsякі не існують у Vendorsтаблиці, і створити резервну копію видалених записів, якщо те, що я роблю, є неправильним, і мені доведеться повернути їх через тиждень-два.


Я б працював над оптимізацією цього запиту і спробував би вліво приєднатися де null
папараццо

Відповіді:


8

План виконання показує, що він читає рядки з некластеризованого індексу в певному порядку, а потім виконує пошуки для кожного зовнішнього рядка, прочитаного для оцінки NOT EXISTS

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

Ви видаляєте 7,2% таблиці. 16 000 000 рядів в 3,556 партіях по 4500

Якщо припустити, що класифіковані рядки події розподіляються по всьому індексу, то це означає, що він буде видаляти приблизно 1 рядок кожні 13,8 рядків.

Таким чином, ітерація 1 прочитає 62156 рядків і виконає те, що багато індексу прагне, перш ніж знайде 4500 для видалення.

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

ітерація 3 буде читати (2 * 57,656) + 62,156 рядків і так далі, поки нарешті ітерація 3,556 прочитає (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 мільйон різних постачальників?
Мартін Сміт

Дякую, Мартін, перевіримо його після 18:00. І ваша відповідь: Це, безумовно, єдиний ПК, що існує в цій таблиці
cilerler

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. Ключове значення полягає в тому, що кожен цикл виконає приблизно стільки ж роботи.
Джо Оббіш

Я маю прийняти іншу відповідь через різницю швидкостей. Я перевірив ваше рішення та рішення @ Мартіна-Сміта, і його версія отримує більше даних ~ 2% в протягом 10 хвилин тесту. Ваші рішення набагато кращі, ніж мої, і я дуже ціную ваш час ... -
cilerler

2

Дві думки спадають на думку:

Затримка, ймовірно, пов'язана з індексуванням цим обсягом даних. Спробуйте скинути індекси, видалити та знову створити індекси.

Або ..

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.