Elimina milioni di righe da una tabella SQL


9

Devo eliminare oltre 16 milioni di record da una tabella di oltre 221 milioni di righe e procede molto lentamente.

Ti ringrazio se condividi suggerimenti per rendere il codice di seguito più veloce:

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

Piano di esecuzione (limitato per 2 iterazioni)

inserisci qui la descrizione dell'immagine

VendorIdè PK e non cluster , dove l' indice cluster non è utilizzato da questo script. Esistono altri 5 indici non univoci e non raggruppati.

L'attività consiste nel "rimuovere i fornitori che non esistono in un'altra tabella" ed eseguirne il backup in un'altra tabella. Ho 3 tabelle, vendors, SpecialVendors, SpecialVendorBackups. Sto cercando di rimuovere quelli SpecialVendorsche non esistono nella Vendorstabella e di avere un backup dei record eliminati nel caso in cui ciò che sto facendo sia sbagliato e devo rimetterli tra una settimana o due.


Lavorerei sull'ottimizzazione di quella query e proverei un join sinistro dove null
paparazzo,

Risposte:


8

Il piano di esecuzione mostra che sta leggendo le righe da un indice non cluster in un certo ordine, quindi esegue ricerche per ogni riga esterna letta per valutare NOT EXISTS

inserisci qui la descrizione dell'immagine

Stai eliminando il 7,2% della tabella. 16.000.000 di file in 3.556 lotti di 4.500

Supponendo che le righe qualificate siano eventualmente distribuite in tutto l'indice, ciò significa che eliminerà circa 1 riga ogni 13,8 righe.

Quindi l'iterazione 1 leggerà 62.156 righe ed eseguirà la ricerca di molti indici prima di trovare 4.500 da eliminare.

iteration 2 leggerà 57.656 (62.156 - 4.500) righe che sicuramente non si qualificheranno ignorando eventuali aggiornamenti simultanei (poiché sono già stati elaborati) e quindi altre 62.156 righe per ottenere 4.500 da eliminare.

l'iterazione 3 leggerà (2 * 57.656) + 62.156 righe e così via fino a quando finalmente l'iterazione 3.556 leggerà (3.555 * 57.656) + 62.156 righe ed eseguirà molte ricerche.

Quindi il numero di ricerche di indice eseguite su tutti i lotti è SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

Che è ((3555 * 3556 / 2) * 57656) + (3556 * 62156)- o364,652,494,976

Suggerirei di materializzare prima le righe da eliminare in una tabella temporanea

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) 

E modifica il DELETEper eliminare WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)Potrebbe essere ancora necessario includere un NOT EXISTSnella DELETEquery stessa per soddisfare gli aggiornamenti poiché la tabella temporanea è stata popolata, ma questo dovrebbe essere molto più efficiente in quanto dovrà eseguire solo 4.500 ricerche per batch.


Quando dici "materializza prima le righe da eliminare in una tabella temporanea", stai suggerendo di posizionare tutti quei record con tutte le loro colonne nella tabella temporanea? o solo la PKcolonna? (Credo che mi stai suggerendo di spostare completamente quelli al tavolo temporaneo ma volevo ricontrollare)
cilerler

@cilerler - Solo le colonne chiave
Martin Smith,

si può rivedere rapidamente questo se ottengo quello che hai detto in modo corretto o no, per favore?
cilerler,

@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTabledovrebbe essere DELETE FROM MySourceTable anche l'indice della tabella temporanea CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );ed è VendorIdsicuramente il PK da solo? Hai> 221 milioni di venditori diversi?
Martin Smith,

Grazie Martin, lo testerò dopo le 18:00. E la tua risposta è: è sicuramente l'unico PK esistente in quella tabella
cilerler il

4

Il piano di esecuzione suggerisce che ogni ciclo successivo farà più lavoro del ciclo precedente. Supponendo che le righe da eliminare siano distribuite uniformemente in tutta la tabella, il primo ciclo dovrà scansionare circa 4500 * 221000000/16000000 = 62156 righe per trovare 4500 righe da eliminare. Farà anche lo stesso numero di ricerche di indice cluster rispetto alla vendortabella. Tuttavia, il secondo ciclo dovrà leggere oltre le stesse 62156 - 4500 = 57656 righe che non sono state eliminate la prima volta. Potremmo aspettarci che il secondo ciclo esegua la scansione di 120000 righe MySourceTablee esegua 120000 ricerche rispetto alla vendortabella. La quantità di lavoro necessaria per ciclo aumenta a una velocità lineare. Come approssimazione possiamo dire che il ciclo medio dovrà leggere 102516868 righe da MySourceTablee fare 102516868 ricerche controvendortavolo. Per eliminare 16 milioni di righe con una dimensione batch di 4500, il codice deve eseguire 16000000/4500 = 3556 loop, quindi la quantità totale di lavoro per il completamento del codice è di circa 364,5 miliardi di righe lette MySourceTablee 364,5 miliardi di ricerche dell'indice.

Un problema minore è che si utilizza una variabile locale @BATCHSIZEin un'espressione TOP senza un RECOMPILEo qualche altro suggerimento. Query Optimizer non conoscerà il valore di quella variabile locale durante la creazione di un piano. Supporrà che sia uguale a 100. In realtà stai eliminando 4500 righe invece di 100 e potresti finire con un piano meno efficiente a causa di quella discrepanza. La stima di cardinalità bassa durante l'inserimento in una tabella può causare anche un impatto sulle prestazioni. SQL Server potrebbe scegliere un'API interna diversa per eseguire gli inserimenti se ritiene di dover inserire 100 righe anziché 4500 righe.

Un'alternativa è semplicemente inserire le chiavi primarie / chiavi raggruppate delle righe che si desidera eliminare in una tabella temporanea. A seconda delle dimensioni delle colonne chiave, questo potrebbe facilmente adattarsi a tempdb. In questo caso è possibile ottenere una registrazione minima, il che significa che il registro delle transazioni non verrà espulso. È inoltre possibile ottenere una registrazione minima su qualsiasi database con un modello di recupero di SIMPLE. Vedere il collegamento per ulteriori informazioni sui requisiti.

Se questa non è un'opzione, è necessario modificare il codice in modo da poter sfruttare l'indice cluster su MySourceTable. La cosa fondamentale è scrivere il codice in modo da eseguire approssimativamente la stessa quantità di lavoro per ciclo. Puoi farlo sfruttando l'indice invece di scansionare la tabella dall'inizio ogni volta. Ho scritto un post sul blog che analizza alcuni diversi metodi di looping. Gli esempi in quel post vengono inseriti in una tabella anziché eliminati ma dovresti essere in grado di adattare il codice.

Nel seguente codice di esempio suppongo che la chiave primaria e la chiave cluster del tuo MySourceTable. Ho scritto questo codice abbastanza rapidamente e non sono in grado di testarlo:

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

La parte chiave è qui:

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

Ogni ciclo leggerà solo 60000 righe da MySourceTable. Ciò dovrebbe comportare una dimensione di eliminazione media di 4500 righe per transazione e una dimensione di eliminazione massima di 60000 righe per transazione. Se vuoi essere più conservativo con una dimensione del lotto inferiore va bene lo stesso. La @STARTIDvariabile avanza dopo ogni ciclo in modo da poter evitare di leggere la stessa riga più di una volta dalla tabella di origine.


Grazie per informazioni dettagliate. Ho impostato quel limite 4500 per non bloccare la tabella. Se non sbaglio SQL ha un limite rigido che blocca l'intera tabella se il conteggio di eliminazione supera 5000. E poiché questo sarà un processo lungo, non posso fare a meno di bloccare quella tabella per un lungo periodo di tempo. Se imposto da 60000 a 4500, pensi che otterrò le stesse prestazioni?
cilerler,

@cilerler Se sei preoccupato per l'escalation dei blocchi, puoi disabilitarlo a livello di tabella. Non c'è niente di sbagliato nell'usare una dimensione batch di 4500. La chiave è che ogni ciclo farà all'incirca la stessa quantità di lavoro.
Joe Obbish,

Devo accettare un'altra risposta a causa delle differenze di velocità. Ho testato la tua soluzione e la soluzione di @ Martin-Smith e la sua versione sta ottenendo più dati del 2% circa per un test di 10 minuti. Le tue soluzioni sono molto migliori delle mie e apprezzo molto per il tuo tempo ... -
cilerler

2

Mi vengono in mente due pensieri:

Il ritardo è probabilmente dovuto all'indicizzazione con quel volume di dati. Prova a eliminare gli indici, a eliminare e a ricostruire gli indici.

O..

Potrebbe essere più veloce copiare le righe che desideri conservare in una tabella temporanea, eliminare la tabella con i 16 milioni di righe e rinominare la tabella temporanea (o copiarla in una nuova istanza della tabella di origine).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.