Löschen Sie Millionen von Zeilen aus einer SQL-Tabelle


9

Ich muss mehr als 16 Millionen Datensätze aus einer Zeile mit mehr als 221 Millionen Zeilen löschen und es geht extrem langsam voran.

Ich freue mich, wenn Sie Vorschläge teilen, um den folgenden Code schneller zu machen:

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

Ausführungsplan (begrenzt auf 2 Iterationen)

Geben Sie hier die Bildbeschreibung ein

VendorIdist PK und nicht geclustert , wobei der Clustered-Index von diesem Skript nicht verwendet wird. Es gibt 5 andere nicht eindeutige, nicht gruppierte Indizes.

Aufgabe ist es, "Anbieter zu entfernen, die in einer anderen Tabelle nicht vorhanden sind" und sie in einer anderen Tabelle zu sichern. Ich habe 3 Tische vendors, SpecialVendors, SpecialVendorBackups. Der Versuch, SpecialVendorsnicht in der VendorsTabelle vorhandene Datensätze zu entfernen und eine Sicherungskopie der gelöschten Datensätze zu erstellen, falls das, was ich tue, falsch ist und ich sie in ein oder zwei Wochen zurücksetzen muss.


Ich würde an der Optimierung dieser Abfrage arbeiten und einen linken Join versuchen, wo null
paparazzo

Antworten:


8

Der Ausführungsplan zeigt, dass er Zeilen aus einem nicht gruppierten Index in einer bestimmten Reihenfolge liest und dann Suchvorgänge für jede gelesene äußere Zeile ausführt, um die zu bewerten NOT EXISTS

Geben Sie hier die Bildbeschreibung ein

Sie löschen 7,2% der Tabelle. 16.000.000 Zeilen in 3.556 Chargen von 4.500

Unter der Annahme, dass die qualifizierten Zeilen tatsächlich im gesamten Index verteilt sind, bedeutet dies, dass alle 13,8 Zeilen ca. 1 Zeile gelöscht wird.

Iteration 1 liest also 62.156 Zeilen und führt so viele Indexsuchen durch, bevor 4.500 zum Löschen gefunden werden.

In Iteration 2 werden 57.656 (62.156 - 4.500) Zeilen gelesen, die definitiv nicht qualifiziert sind, wenn gleichzeitige Aktualisierungen ignoriert werden (da sie bereits verarbeitet wurden), und dann weitere 62.156 Zeilen, um 4.500 zu löschen.

Iteration 3 liest (2 * 57.656) + 62.156 Zeilen und so weiter, bis schließlich Iteration 3.556 (3.555 * 57.656) + 62.156 Zeilen liest und so viele Suchvorgänge ausführt.

Die Anzahl der Indexsuchen, die über alle Stapel hinweg durchgeführt werden, beträgt also SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

Welches ist ((3555 * 3556 / 2) * 57656) + (3556 * 62156)- oder364,652,494,976

Ich würde vorschlagen, dass Sie die zu löschenden Zeilen zuerst in einer temporären Tabelle materialisieren

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) 

Und ändern Sie das DELETEzu löschen. WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)Möglicherweise müssen Sie noch ein NOT EXISTSin die DELETEAbfrage selbst aufnehmen, um Aktualisierungen zu berücksichtigen, da die temporäre Tabelle ausgefüllt wurde. Dies sollte jedoch wesentlich effizienter sein, da nur 4.500 Suchvorgänge pro Stapel ausgeführt werden müssen.


Wenn Sie sagen "Materialisieren Sie die Zeilen, die zuerst in einer temporären Tabelle gelöscht werden sollen", schlagen Sie vor, alle diese Datensätze mit ihren gesamten Spalten in eine temporäre Tabelle zu platzieren? oder nur die PKSpalte? (Ich glaube, Sie schlagen mir vor, diese vollständig in die
temporäre

@cilerler - Nur die Schlüsselspalte (n)
Martin Smith

können Sie schnell überprüfen diese , wenn ich bekommen , was Sie gesagt haben richtig oder nicht, bitte?
Cilerler

@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTablesollte nur DELETE FROM MySourceTable auch die temporäre Tabelle indizieren CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );und ist VendorIddefinitiv die PK für sich? Sie haben> 221 Millionen verschiedene Anbieter?
Martin Smith

Danke Martin, werde es nach 18 Uhr testen. Und Ihre Antwort lautet: Es ist definitiv die einzige PK in dieser Tabelle
Cilerler

4

Der Ausführungsplan schlägt vor, dass jede aufeinanderfolgende Schleife mehr Arbeit leistet als die vorherige Schleife. Unter der Annahme, dass die zu löschenden Zeilen gleichmäßig in der Tabelle verteilt sind, muss die erste Schleife etwa 4500 * 221000000/16000000 = 62156 Zeilen scannen, um 4500 zu löschende Zeilen zu finden. Es wird auch die gleiche Anzahl von Clustered-Index-Suchvorgängen für die vendorTabelle ausgeführt. Die zweite Schleife muss jedoch über dieselben Zeilen von 62156 - 4500 = 57656 hinaus lesen, die Sie beim ersten Mal nicht gelöscht haben. Es ist zu erwarten, dass die zweite Schleife 120000 Zeilen aus MySourceTableder vendorTabelle scannt und 120000 Suchvorgänge für die Tabelle ausführt . Der Arbeitsaufwand pro Schleife nimmt linear zu. Als Annäherung können wir sagen, dass die durchschnittliche Schleife 102516868 Zeilen von MySourceTableund lesen muss, um 102516868- Suchvorgänge gegen die durchzuführenvendorTabelle. Um 16 Millionen Zeilen mit einer Stapelgröße von 4500 zu löschen, muss Ihr Code 16000000/4500 = 3556 Schleifen ausführen. Der Gesamtaufwand für die Fertigstellung Ihres Codes beträgt also rund 364,5 Milliarden Zeilen, aus MySourceTabledenen gelesen wird, und 364,5 Milliarden Indexsuchen.

Ein kleineres Problem ist, dass Sie eine lokale Variable @BATCHSIZEin einem TOP-Ausdruck ohne einen RECOMPILEoder einen anderen Hinweis verwenden. Der Abfrageoptimierer kennt den Wert dieser lokalen Variablen beim Erstellen eines Plans nicht. Es wird davon ausgegangen, dass es gleich 100 ist. In Wirklichkeit löschen Sie 4500 Zeilen anstelle von 100, und aufgrund dieser Diskrepanz könnten Sie möglicherweise einen weniger effizienten Plan erhalten. Die niedrige Kardinalitätsschätzung beim Einfügen in eine Tabelle kann ebenfalls zu einem Leistungseinbruch führen. SQL Server wählt möglicherweise eine andere interne API zum Einfügen aus, wenn es der Meinung ist, dass 100 Zeilen anstelle von 4500 Zeilen eingefügt werden müssen.

Eine Alternative besteht darin, einfach die Primärschlüssel / Clusterschlüssel der Zeilen, die Sie löschen möchten, in eine temporäre Tabelle einzufügen. Abhängig von der Größe Ihrer Schlüsselspalten kann dies leicht in Tempdb passen. In diesem Fall können Sie nur eine minimale Protokollierung erhalten , was bedeutet, dass das Transaktionsprotokoll nicht explodiert. Sie können auch eine minimale Protokollierung für jede Datenbank mit einem Wiederherstellungsmodell von erhalten SIMPLE. Weitere Informationen zu den Anforderungen finden Sie unter dem Link.

Wenn dies keine Option ist, sollten Sie Ihren Code ändern, damit Sie den Clustered-Index für nutzen können MySourceTable. Der Schlüssel ist, Ihren Code so zu schreiben, dass Sie ungefähr die gleiche Menge Arbeit pro Schleife erledigen. Sie können dies tun, indem Sie den Index nutzen, anstatt die Tabelle jedes Mal von Anfang an zu scannen. Ich habe einen Blog-Beitrag geschrieben , in dem verschiedene Methoden des Loopings behandelt werden. Die Beispiele in diesem Beitrag fügen zwar in eine Tabelle ein, anstatt sie zu löschen, aber Sie sollten in der Lage sein, den Code anzupassen.

Im folgenden Beispielcode gehe ich davon aus, dass der Primärschlüssel und der Clusterschlüssel von Ihnen MySourceTable. Ich habe diesen Code ziemlich schnell geschrieben und kann ihn nicht testen:

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

Der Schlüsselteil ist hier:

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

Jede Schleife liest nur 60000 Zeilen aus MySourceTable. Dies sollte zu einer durchschnittlichen Löschgröße von 4500 Zeilen pro Transaktion und einer maximalen Löschgröße von 60000 Zeilen pro Transaktion führen. Wenn Sie mit einer kleineren Losgröße konservativer sein möchten, ist das auch in Ordnung. Die @STARTIDVariable rückt nach jeder Schleife vor, sodass Sie vermeiden können, dass dieselbe Zeile mehrmals aus der Quelltabelle gelesen wird.


Vielen Dank für detaillierte Informationen. Ich habe diese 4500-Grenze so eingestellt, dass die Tabelle nicht gesperrt wird. Wenn ich mich nicht irre, hat SQL ein hartes Limit, das die gesamte Tabelle sperrt, wenn die Löschanzahl über 5000 liegt. Und da dies ein langer Prozess ist, kann ich mich nicht bemühen, diese Tabelle für einen langen Zeitraum zu sperren. Glaubst du, wenn ich 60000 bis 4500 einstelle, bekomme ich die gleiche Leistung?
Cilerler

@cilerler Wenn Sie sich Sorgen über die Eskalation von Sperren machen, können Sie diese auf Tabellenebene deaktivieren. Es ist nichts Falsches daran, eine Stapelgröße von 4500 zu verwenden. Der Schlüssel ist, dass jede Schleife ungefähr die gleiche Menge an Arbeit erledigt.
Joe Obbish

Ich muss aufgrund der Geschwindigkeitsunterschiede andere Antworten akzeptieren. Ich habe Ihre Lösung und die Lösung von @ Martin-Smith getestet und seine Version liefert mehr Daten ~ 2% für einen 10-minütigen Test. Ihre Lösungen sind viel besser als meine und ich schätze es wirklich für Ihre Zeit ... -
Cilerler

2

Zwei Gedanken fallen mir ein:

Die Verzögerung ist wahrscheinlich auf die Indizierung mit diesem Datenvolumen zurückzuführen. Versuchen Sie, die Indizes zu löschen, zu löschen und neu zu erstellen.

Oder..

Es kann schneller sein, die Zeilen, die Sie behalten möchten, in eine temporäre Tabelle zu kopieren, die Tabelle mit den 16 Millionen Zeilen zu löschen und die temporäre Tabelle umzubenennen (oder in eine neue Instanz der Quelltabelle zu kopieren).

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.