web-dev-qa-db-fra.com

Supprimer des millions de lignes d'une table SQL

Je dois supprimer 16+ millions d'enregistrements d'une table de 221+ millions de lignes et cela va extrêmement lentement.

J'apprécie si vous partagez des suggestions pour accélérer le code ci-dessous:

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 d'exécution (limité pour 2 itérations)

enter image description here

VendorId est [~ # ~] pk [~ # ~] et non groupé , où l'index cluster n'est pas utilisé par ce script. Il existe 5 autres index non uniques et non groupés.

La tâche consiste à "supprimer les fournisseurs qui n'existent pas dans une autre table" et à les sauvegarder dans une autre table. J'ai 3 tables, vendors, SpecialVendors, SpecialVendorBackups. Essayer de supprimer SpecialVendors qui n'existent pas dans la table Vendors, et d'avoir une sauvegarde des enregistrements supprimés au cas où ce que je fais est mal et je dois les remettre dans une semaine ou deux.

9
cilerler

Le plan d'exécution montre qu'il lit les lignes d'un index non cluster dans un certain ordre, puis effectue des recherches pour chaque ligne externe lue afin d'évaluer le NOT EXISTS

enter image description here

Vous supprimez 7,2% du tableau. 16 000 000 lignes en 3 556 lots de 4 500

En supposant que les lignes qualifiées soient éventuellement réparties dans l'index, cela signifie qu'il supprimera environ 1 ligne toutes les 13,8 lignes.

Ainsi, l'itération 1 lira 62 156 lignes et effectuera la recherche de nombreux index avant de trouver 4 500 à supprimer.

l'itération 2 lira 57 656 (62 156 - 4 500) lignes qui ne seront certainement pas qualifiées en ignorant les mises à jour simultanées (car elles ont déjà été traitées), puis encore 62 156 lignes pour obtenir 4 500 à supprimer.

l'itération 3 lira (2 * 57 656) + 62 156 lignes et ainsi de suite jusqu'à ce que finalement l'itération 3 556 lira (3 555 * 57 656) + 62 156 lignes et effectuera ce que beaucoup recherchent.

Le nombre de recherches d'index effectuées sur tous les lots est donc SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

Qui est ((3555 * 3556 / 2) * 57656) + (3556 * 62156) - ou 364,652,494,976

Je suggère que vous matérialisiez d'abord les lignes à supprimer dans une table temporaire

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) 

Et modifiez DELETE pour supprimer WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber) Vous devrez peut-être toujours inclure un NOT EXISTS Dans la requête DELETE elle-même pour répondre aux mises à jour depuis la table temporaire. mais cela devrait être beaucoup plus efficace car il n'aura besoin que de 4500 recherches par lot.

8
Martin Smith

Le plan d'exécution suggère que chaque boucle successive fera plus de travail que la boucle précédente. En supposant que les lignes à supprimer sont réparties uniformément dans le tableau, la première boucle devra analyser environ 4500 * 221000000/16000000 = 62156 lignes pour trouver 4500 lignes à supprimer. Il effectuera également le même nombre de recherches d'index cluster sur la table vendor. Cependant, la deuxième boucle devra lire au-delà des mêmes lignes 62156 - 4500 = 57656 que vous n'avez pas supprimées la première fois. Nous pouvons nous attendre à ce que la deuxième boucle analyse 120000 lignes à partir de MySourceTable et effectue 120000 recherches par rapport à la table vendor. La quantité de travail nécessaire par boucle augmente à un rythme linéaire. Comme approximation, nous pouvons dire que la boucle moyenne devra lire 102516868 lignes à partir de MySourceTable et faire 102516868 chercher par rapport à la table vendor. Pour supprimer 16 millions de lignes avec une taille de lot de 4500, votre code doit effectuer 16000000/4500 = 3556 boucles, donc la quantité totale de travail à effectuer pour votre code est d'environ 364,5 milliards de lignes lues à partir de MySourceTable et 364,5 milliards index recherche.

Un problème plus petit est que vous utilisez une variable locale @BATCHSIZE dans une expression TOP sans RECOMPILE ou autre indice. L'optimiseur de requêtes ne connaîtra pas la valeur de cette variable locale lors de la création d'un plan. Il supposera qu'il est égal à 100. En réalité, vous supprimez 4500 lignes au lieu de 100, et vous pourriez éventuellement vous retrouver avec un plan moins efficace en raison de cet écart. La faible estimation de cardinalité lors de l'insertion dans une table peut également entraîner une baisse des performances. SQL Server peut choisir une API interne différente pour effectuer des insertions s'il pense qu'il doit insérer 100 lignes au lieu de 4500 lignes.

Une alternative consiste à simplement insérer les clés primaires/clés en cluster des lignes que vous souhaitez supprimer dans une table temporaire. Selon la taille de vos colonnes clés, cela pourrait facilement s'intégrer dans tempdb. Vous pouvez obtenir journalisation minimale dans ce cas, ce qui signifie que le journal des transactions ne va pas exploser. Vous pouvez également obtenir une journalisation minimale sur n'importe quelle base de données avec un modèle de récupération de SIMPLE. Voir le lien pour plus d'informations sur les exigences.

Si ce n'est pas une option, vous devez modifier votre code afin de pouvoir profiter de l'index cluster sur MySourceTable. L'essentiel est d'écrire votre code afin que vous fassiez environ la même quantité de travail par boucle. Vous pouvez le faire en profitant de l'index au lieu de simplement balayer la table depuis le début à chaque fois. J'ai écrit un article de blog qui passe en revue différentes méthodes de bouclage. Les exemples de cette publication insèrent dans une table au lieu de supprimer, mais vous devriez pouvoir adapter le code.

Dans l'exemple de code ci-dessous, je suppose que la clé primaire et la clé en cluster de votre MySourceTable. J'ai écrit ce code assez rapidement et je ne suis pas en mesure de le tester:

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 partie clé est ici:

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

Chaque boucle ne lira que 60000 lignes de MySourceTable. Cela devrait entraîner une taille de suppression moyenne de 4500 lignes par transaction et une taille de suppression maximale de 60000 lignes par transaction. Si vous voulez être plus conservateur avec une taille de lot plus petite, c'est bien aussi. Le @STARTID la variable avance après chaque boucle afin que vous puissiez éviter de lire la même ligne plusieurs fois dans la table source.

4
Joe Obbish

Deux pensées me viennent à l'esprit:

Le retard est probablement dû à l'indexation avec ce volume de données. Essayez de supprimer les index, de les supprimer et de les reconstruire.

Ou..

Il peut être plus rapide de copier les lignes que vous souhaitez conserver dans une table temporaire, de supprimer la table avec les 16 millions de lignes et de renommer la table temporaire (ou de la copier dans une nouvelle instance de la table source).

2
Jon