Supprimer des millions de lignes d'une table SQL

9

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é à 2 itérations)

entrez la description de l'image ici

VendorIdest PK et non en cluster , où l' index en 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 ceux SpecialVendorsqui n'existent pas dans le Vendorstableau, 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.

cilerler
la source
Je voudrais travailler sur l'optimisation de cette requête et essayer une jointure gauche où null
paparazzo

Réponses:

8

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 la NOT EXISTS

entrez la description de l'image ici

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

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

L'itération 1 lira donc 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)- ou364,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 le DELETEpour supprimer WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)Vous devrez peut-être toujours inclure un NOT EXISTSdans la DELETErequête elle-même pour répondre aux mises à jour depuis que la table temporaire a été remplie, mais cela devrait être beaucoup plus efficace car il n'aura besoin que de 4500 recherches par lot.

Martin Smith
la source
Lorsque vous dites "matérialiser d'abord les lignes à supprimer dans une table temporaire", proposez-vous de placer tous ces enregistrements avec toutes leurs colonnes dans la table temporaire? ou seulement la PKcolonne? (Je crois que vous me proposez de les déplacer complètement vers la table temporaire, mais vous vouliez vérifier à nouveau)
cilerler
@cilerler - Juste la ou les colonnes clés
Martin Smith
pouvez - vous passer rapidement en revue ce si je reçois ce que vous avez dit correctement ou non, s'il vous plaît?
cilerler
@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTabledevrait juste être DELETE FROM MySourceTable aussi indexer la table temporaire CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );et est VendorIdcertainement le PK seul? Vous avez> 221 millions de fournisseurs différents?
Martin Smith
Merci Martin, le testera après 18h. Et votre réponse est, c'est certainement le seul PK existant dans cette table
cilerler
4

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 vendortable. 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 MySourceTableet effectue 120000 recherches par rapport à la vendortable. La quantité de travail nécessaire par boucle augmente à un rythme linéaire. En tant qu'approximation, nous pouvons dire que la boucle moyenne devra lire 102516868 lignes depuis MySourceTableet pour faire 102516868 cherche par rapport à lavendortable. Pour supprimer 16 millions de lignes avec une taille de lot de 4500, votre code doit faire 16000000/4500 = 3556 boucles, donc la quantité totale de travail pour votre code est d'environ 364,5 milliards de lignes lues MySourceTableet 364,5 milliards d'index recherchés.

Un problème plus petit est que vous utilisez une variable locale @BATCHSIZEdans une expression TOP sans un RECOMPILEou un 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. L'estimation de faible 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. En fonction de la taille de vos colonnes clés, cela pourrait facilement s'intégrer dans tempdb. Dans ce cas, vous pouvez obtenir une journalisation minimale, ce qui signifie que le journal des transactions ne explosera pas. 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 clusterisé 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 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. La @STARTIDvariable avance après chaque boucle afin que vous puissiez éviter de lire la même ligne plus d'une fois dans la table source.

Joe Obbish
la source
Merci pour les informations détaillées. J'ai défini cette limite de 4500 pour ne pas verrouiller la table. Si je ne me trompe pas, SQL a une limite stricte qui verrouille toute la table si le nombre de suppressions dépasse 5000. Et comme ce sera un long processus, je ne peux pas m'efforcer de verrouiller cette table pendant une longue période. Si je mets ce 60000 à 4500, pensez-vous que j'obtiendrai les mêmes performances?
cilerler
@cilerler Si vous vous inquiétez de l'escalade des verrous, vous pouvez la désactiver au niveau de la table. Il n'y a rien de mal à utiliser une taille de lot de 4500. La clé est que chaque boucle fera à peu près la même quantité de travail.
Joe Obbish
Je dois accepter une autre réponse en raison des différences de vitesse. J'ai testé votre solution et la solution de @ Martin-Smith et sa version obtient plus de données ~ 2% pour un test de 10 minutes. Vos solutions sont bien meilleures que les miennes et j'apprécie vraiment votre temps ... -
cilerler
2

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 supprimer et de reconstruire les index.

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).

Jon
la source