Eliminar millones de filas de una tabla SQL

9

Tengo que eliminar más de 16 millones de registros de una tabla de filas de más de 221 millones y va extremadamente lento.

Le agradezco si comparte sugerencias para hacer el siguiente código más rápido:

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 de ejecución (limitado para 2 iteraciones)

ingrese la descripción de la imagen aquí

VendorIdes PK y no está agrupado , donde este script no utiliza el índice agrupado . Hay otros 5 índices no únicos y no agrupados.

La tarea es "eliminar proveedores que no existen en otra tabla" y hacer una copia de seguridad en otra tabla. Tengo 3 tablas, vendors, SpecialVendors, SpecialVendorBackups. Intento eliminar los SpecialVendorsque no existen en la Vendorstabla y tener una copia de seguridad de los registros eliminados en caso de que lo que estoy haciendo esté mal y tenga que volver a colocarlos en una o dos semanas.

cilerler
fuente
Trabajaría en la optimización de esa consulta e intentaría una unión izquierda donde nulo
paparazzo

Respuestas:

8

El plan de ejecución muestra que está leyendo filas de un índice no agrupado en algún orden y luego realiza búsquedas para cada lectura de fila externa para evaluar el NOT EXISTS

ingrese la descripción de la imagen aquí

Estás eliminando el 7.2% de la tabla. 16,000,000 filas en 3,556 lotes de 4,500

Suponiendo que las filas que califican se distribuyen eventualmente en todo el índice, esto significa que eliminará aproximadamente 1 fila cada 13.8 filas.

Entonces, la iteración 1 leerá 62,156 filas y realizará tantas búsquedas de índice antes de encontrar 4,500 para eliminar.

la iteración 2 leerá 57.656 (62.156 - 4.500) filas que definitivamente no calificarán ignorando las actualizaciones concurrentes (ya que ya se han procesado) y luego otras 62.156 filas para obtener 4.500 para eliminar.

la iteración 3 leerá (2 * 57.656) + 62.156 filas y así sucesivamente hasta que finalmente la iteración 3.556 leerá (3.555 * 57.656) + 62.156 filas y realice tantas búsquedas.

Por lo tanto, el número de búsquedas de índice realizadas en todos los lotes es SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

Cuál es ((3555 * 3556 / 2) * 57656) + (3556 * 62156)- o364,652,494,976

Sugeriría que materialice las filas para eliminar primero en una tabla temporal

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) 

Y cambie la DELETEopción para eliminar WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)Es posible que aún deba incluir un NOT EXISTSen la DELETEconsulta para atender las actualizaciones, ya que la tabla temporal se completó, pero esto debería ser mucho más eficiente, ya que solo tendrá que realizar 4.500 búsquedas por lote.

Martin Smith
fuente
Cuando dice "materializar las filas para eliminar primero en una tabla temporal", ¿sugiere colocar todos esos registros con todas sus columnas en la tabla temporal? o solo la PKcolumna? (Creo que me está sugiriendo para mover los de tabla temporal por completo, pero quería comprobarlo)
cilerler
@cilerler - Just the key column (s)
Martin Smith
¿Puedes revisar esto rápidamente si recibo lo que dijiste correctamente o no, por favor?
Cilerler
@cilerler: DELETE TOP (@BATCHSIZE) FROM MySourceTable¿ DELETE FROM MySourceTable también debería indexar la tabla temporal CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );y VendorIddefinitivamente es la PK por sí sola? ¿Tiene> 221 millones de vendedores diferentes?
Martin Smith
Gracias Martin, lo probaré después de las 6 p.m. Y su respuesta es: definitivamente es el único PK que existe en esa tabla
cilerler
4

El plan de ejecución sugiere que cada ciclo sucesivo hará más trabajo que el ciclo anterior. Suponiendo que las filas para eliminar se distribuyen uniformemente en toda la tabla, el primer bucle deberá escanear alrededor de 4500 * 221000000/16000000 = 62156 filas para encontrar 4500 filas para eliminar. También hará el mismo número de búsquedas de índice agrupado contra la vendortabla. Sin embargo, el segundo ciclo deberá leer más allá de las mismas 62156 - 4500 = 57656 filas que no eliminó la primera vez. Podríamos esperar que el segundo bucle escanee 120000 filas MySourceTabley realice 120000 búsquedas contra la vendormesa. La cantidad de trabajo necesaria por ciclo aumenta a una velocidad lineal. Como una aproximación, podemos decir que el ciclo promedio necesitará leer 102516868 filas desde MySourceTabley para hacer 102516868 busca en contra de lavendormesa. Para eliminar 16 millones de filas con un tamaño de lote de 4500, su código debe hacer 16000000/4500 = 3556 bucles, por lo que la cantidad total de trabajo para completar su código es de alrededor de 364.5 mil millones de filas leídas MySourceTabley 364.5 mil millones de búsquedas de índice.

Un problema menor es que usa una variable local @BATCHSIZEen una expresión TOP sin una RECOMPILEu otra sugerencia. El optimizador de consultas no sabrá el valor de esa variable local al crear un plan. Asumirá que es igual a 100. En realidad, está eliminando 4500 filas en lugar de 100, y posiblemente podría terminar con un plan menos eficiente debido a esa discrepancia. La estimación de baja cardinalidad al insertar en una tabla también puede causar un impacto en el rendimiento. SQL Server podría elegir una API interna diferente para hacer inserciones si cree que necesita insertar 100 filas en lugar de 4500 filas.

Una alternativa es simplemente insertar las claves principales / claves agrupadas de las filas que desea eliminar en una tabla temporal. Dependiendo del tamaño de sus columnas clave, esto podría encajar fácilmente en tempdb. Puede obtener un registro mínimo en ese caso, lo que significa que el registro de transacciones no explotará. También puede obtener un registro mínimo en cualquier base de datos con un modelo de recuperación de SIMPLE. Consulte el enlace para obtener más información sobre los requisitos.

Si esa no es una opción, entonces debe cambiar su código para poder aprovechar el índice agrupado MySourceTable. La clave es escribir su código para que haga aproximadamente la misma cantidad de trabajo por ciclo. Puede hacerlo aprovechando el índice en lugar de simplemente escanear la tabla desde el principio cada vez. Escribí una publicación de blog que analiza algunos métodos diferentes de bucle. Los ejemplos en esa publicación se insertan en una tabla en lugar de eliminarse, pero debería poder adaptar el código.

En el código de ejemplo a continuación, supongo que la clave principal y la clave agrupada de su MySourceTable. Escribí este código bastante rápido y no puedo probarlo:

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 clave está aquí:

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

Cada ciclo solo leerá 60000 filas MySourceTable. Eso debería dar como resultado un tamaño de eliminación promedio de 4500 filas por transacción y un tamaño de eliminación máximo de 60000 filas por transacción. Si quieres ser más conservador con un tamaño de lote más pequeño, también está bien. La @STARTIDvariable avanza después de cada ciclo para que pueda evitar leer la misma fila más de una vez desde la tabla de origen.

Joe Obbish
fuente
Gracias por la información detallada. Establecí ese límite de 4500 para no bloquear la mesa. Si no me equivoco, SQL tiene un límite estricto que bloquea la tabla completa si el recuento de eliminación supera los 5000. Y dado que este será un proceso largo, no puedo esforzarme por bloquear esa tabla durante un largo período de tiempo. Si configuro ese 60000 a 4500, ¿crees que obtendré el mismo rendimiento?
Cilerler
@cilerler Si le preocupa la escalada de bloqueo, puede deshabilitarla a nivel de tabla. No hay nada de malo en usar un tamaño de lote de 4500. La clave es que cada ciclo hará aproximadamente la misma cantidad de trabajo.
Joe Obbish
Tengo que aceptar otra respuesta debido a las diferencias de velocidad. Probé su solución y la solución de @ Martin-Smith y su versión está obteniendo más datos ~ 2% en una prueba de 10 minutos. Sus soluciones son mucho mejor que la mía y yo realmente aprecio por su tiempo ... -
cilerler
2

Dos pensamientos me vienen a la mente:

El retraso probablemente se deba a la indexación con ese volumen de datos. Intente soltar los índices, eliminar y reconstruir los índices.

O..

Puede ser más rápido copiar las filas que desea mantener en una tabla temporal, soltar la tabla con los 16 millones de filas y cambiar el nombre de la tabla temporal (o copiar a una nueva instancia de la tabla fuente).

Jon
fuente