Usuń miliony wierszy z tabeli SQL

9

Muszę usunąć ponad 16 milionów rekordów z tabeli ponad 221 milionów wierszy i idzie to bardzo wolno.

Doceniam, jeśli podzielisz się sugestiami, aby kod poniżej był szybszy:

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

Plan wykonania (ograniczony do 2 iteracji)

wprowadź opis zdjęcia tutaj

VendorIdjest PK i nieklastrowany , gdzie indeks klastrowany nie jest używany przez ten skrypt. Istnieje 5 innych nieunikalnych indeksów nieklastrowych.

Zadanie polega na „usunięciu dostawców, którzy nie istnieją w innej tabeli” i skopiowaniu ich do innej tabeli. Mam 3 tabel vendors, SpecialVendors, SpecialVendorBackups. Próbuję usunąć te, SpecialVendorsktórych nie ma w Vendorstabeli, i wykonać kopię zapasową usuniętych rekordów na wypadek, gdyby to, co robię, było złe i musiałem je przywrócić za tydzień lub dwa.

cilerler
źródło
Chciałbym zoptymalizować to zapytanie i wypróbować lewe łączenie, gdzie null
paparazzo

Odpowiedzi:

8

Plan wykonania pokazuje, że odczytuje wiersze z indeksu nieklastrowanego w określonej kolejności, a następnie wykonuje wyszukiwanie dla każdego odczytanego wiersza zewnętrznego w celu oceny NOT EXISTS

wprowadź opis zdjęcia tutaj

Usuwasz 7,2% tabeli. 16 000 000 wierszy w 3556 partiach po 4500

Zakładając, że kwalifikujące się wiersze są oczywiście rozmieszczone w całym indeksie, oznacza to, że usunie około 1 wiersza co 13,8 wierszy.

Tak więc iteracja 1 odczyta 62 156 wierszy i wykona tyle wyszukiwań indeksu, zanim znajdzie 4500 do usunięcia.

iteracja 2 odczyta 57 656 (62 156 - 4500) wierszy, które zdecydowanie nie kwalifikują się do zignorowania jakichkolwiek równoczesnych aktualizacji (ponieważ zostały już przetworzone), a następnie kolejne 62 156 wierszy, aby 4500 usunięto.

iteracja 3 odczyta (2 * 57 656) + 62 156 wierszy i tak dalej, aż w końcu iteracja 3556 odczyta (3555 * 57 656) + 62 156 wierszy i wykona tyle prób.

Tak więc liczba wyszukiwań indeksu wykonanych we wszystkich partiach wynosi SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

Który jest ((3555 * 3556 / 2) * 57656) + (3556 * 62156)- lub364,652,494,976

Sugerowałbym, aby najpierw zmaterializować wiersze do usunięcia w tabeli tymczasowej

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) 

I zmienić DELETE, aby usunąć WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)Nadal może trzeba obejmują NOT EXISTSw DELETEzapytaniu samego celu zaspokojenia aktualizacjach ponieważ tabela temp została wypełniona, ale to powinno być znacznie bardziej skuteczne, jak to tylko będzie trzeba wykonać 4500 ma na partię.

Martin Smith
źródło
Kiedy mówisz „zmaterializuj wiersze, które chcesz najpierw usunąć do tabeli tymczasowej”, sugerujesz, aby umieścić wszystkie te rekordy ze wszystkimi kolumnami w tabeli tymczasowej? czy tylko PKkolumna? (
Wydaje
@ cilerler - Tylko kluczowe kolumny
Martin Smith
czy możesz szybko to sprawdzić, jeśli rozumiem poprawnie, czy nie?
cilerler
@ cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTablepowinien po prostu DELETE FROM MySourceTable indeksować tabelę temp CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );i czy VendorIdzdecydowanie PK jest sam w sobie? Masz> 221 milionów różnych dostawców?
Martin Smith
Dzięki Martin, przetestuję to po 18:00. A twoja odpowiedź brzmi: to zdecydowanie jedyny PK w tej tabeli
cilerler
4

Plan wykonania sugeruje, że każda kolejna pętla wykona więcej pracy niż poprzednia. Zakładając, że wiersze do usunięcia są równomiernie rozmieszczone w całej tabeli, pierwsza pętla będzie musiała zeskanować około 4500 * 221000000/16000000 = 62156 wierszy, aby znaleźć 4500 wierszy do usunięcia. Wykona również tę samą liczbę wyszukiwań indeksu klastrowego względem vendortabeli. Jednak druga pętla będzie musiała odczytać te same wiersze 62156 - 4500 = 57656, które nie zostały usunięte za pierwszym razem. Możemy się spodziewać, że druga pętla przeskanuje 120000 wierszy zi MySourceTablewykona 120000 wyszukiwań względem vendortabeli. Ilość pracy potrzebnej na pętlę rośnie w tempie liniowym. W przybliżeniu możemy powiedzieć, że średnia pętla będzie musiała odczytać 102516868 wierszy zi MySourceTabledo 102516868 szuka względemvendorstół. Aby usunąć 16 milionów wierszy o wielkości partii 4500, kod musi wykonać 16000000/4500 = 3556 pętli, więc łączna ilość pracy do wykonania przez kod wynosi około 364,5 miliarda wierszy odczytanych MySourceTablei 364,5 miliarda indeksów.

Mniejszy problem polega na tym, że używasz zmiennej lokalnej @BATCHSIZEw wyrażeniu TOP bez żadnej RECOMPILEinnej wskazówki. Optymalizator zapytań nie pozna wartości tej zmiennej lokalnej podczas tworzenia planu. Zakłada się, że jest równa 100. W rzeczywistości usuwasz 4500 wierszy zamiast 100, i możesz ewentualnie skończyć z mniej wydajnym planem z powodu tej rozbieżności. Niska ocena liczności przy wstawianiu do tabeli może również spowodować obniżenie wydajności. SQL Server może wybrać inny wewnętrzny interfejs API do wstawiania, jeśli uzna, że ​​musi wstawić 100 wierszy zamiast 4500 wierszy.

Jedną z możliwości jest po prostu wstawienie kluczy podstawowych / kluczy klastrowych wierszy, które chcesz usunąć, do tabeli tymczasowej. W zależności od wielkości twoich kluczowych kolumn może to łatwo zmieścić się w tempdb. W takim przypadku możesz uzyskać minimalne logowanie , co oznacza, że ​​dziennik transakcji nie zostanie wysadzony. Możesz także uzyskać minimalne rejestrowanie w dowolnej bazie danych z modelem odzyskiwania SIMPLE. Zobacz link, aby uzyskać więcej informacji na temat wymagań.

Jeśli nie jest to opcja, należy zmienić kod, aby móc skorzystać z indeksu klastrowego MySourceTable. Kluczową sprawą jest napisanie kodu, aby wykonać mniej więcej tyle samo pracy na pętlę. Możesz to zrobić, wykorzystując indeks zamiast za każdym razem skanować tabelę od początku. Napisałem post na blogu, który omawia różne metody zapętlania. Przykłady w tym poście zamiast wstawiania wstawiają do tabeli, ale powinieneś być w stanie dostosować kod.

W przykładowym kodzie poniżej zakładam, że klucz podstawowy i klucz klastrowany twojego MySourceTable. Napisałem ten kod dość szybko i nie jestem w stanie go przetestować:

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

Kluczowa część jest tutaj:

WITH MySourceTable_DELCTE AS (
    SELECT TOP (60000) *
    FROM MySourceTable
    WHERE ID >= @STARTID
    ORDER BY ID
)   

Każda pętla odczyta tylko 60000 wierszy MySourceTable. Powinno to spowodować średni rozmiar usuwania wynoszący 4500 wierszy na transakcję i maksymalny rozmiar usuwania wynoszący 60000 wierszy na transakcję. Jeśli chcesz być bardziej konserwatywny z mniejszym rozmiarem partii, to też jest w porządku. Te @STARTIDzmienne przesunie się po każdej pętli, dzięki czemu można uniknąć czytając ten sam wiersz więcej niż raz z tabeli źródłowej.

Joe Obbish
źródło
Dziękuję za szczegółowe informacje. Ustawiłem limit 4500, aby nie blokować tabeli. Jeśli się nie mylę, SQL ma twardy limit, który blokuje całą tabelę, jeśli liczba usunięć przekroczy 5000. A ponieważ będzie to długi proces, nie mogę się starać blokować tej tabeli przez długi okres czasu. Jeśli ustawię 60000 na 4500, czy myślisz, że uzyskam taką samą wydajność?
cilerler
@ cilerler Jeśli martwisz się eskalacją blokady, możesz ją wyłączyć na poziomie tabeli. Nie ma nic złego w stosowaniu wielkości partii 4500. Kluczem jest to, że każda pętla wykona mniej więcej taką samą ilość pracy.
Joe Obbish
Muszę zaakceptować inną odpowiedź z powodu różnic prędkości. Przetestowałem twoje rozwiązanie i rozwiązanie @ Martina-Smitha, a jego wersja uzyskuje więcej danych ~ 2% w ciągu 10 minut testu. Twoje rozwiązania są znacznie lepsze niż moje i naprawdę doceniam twój czas ... -
cilerler
2

Przychodzą mi na myśl dwie myśli:

Opóźnienie jest prawdopodobnie spowodowane indeksowaniem z taką ilością danych. Spróbuj upuścić indeksy, usunąć i ponownie zbudować indeksy.

Lub..

Szybsze może być skopiowanie wierszy, które chcesz zachować, do tabeli tymczasowej, upuszczenie tabeli z 16 milionami wierszy i zmiana nazwy tabeli tymczasowej (lub skopiowanie do nowej instancji tabeli źródłowej).

Jon
źródło