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)
VendorId
est 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 SpecialVendors
qui n'existent pas dans le Vendors
tableau, 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.
la source
Réponses:
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
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
Et modifiez le
DELETE
pour supprimerWHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)
Vous devrez peut-être toujours inclure unNOT EXISTS
dans laDELETE
requê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.la source
PK
colonne? (Je crois que vous me proposez de les déplacer complètement vers la table temporaire, mais vous vouliez vérifier à nouveau)DELETE TOP (@BATCHSIZE) FROM MySourceTable
devrait juste êtreDELETE FROM MySourceTable
aussi indexer la table temporaireCREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );
et estVendorId
certainement le PK seul? Vous avez> 221 millions de fournisseurs différents?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
vendor
table. 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 lignesMySourceTable
et effectue 120000 recherches par rapport à lavendor
table. 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 depuisMySourceTable
et pour faire 102516868 cherche par rapport à lavendor
table. 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 luesMySourceTable
et 364,5 milliards d'index recherchés.Un problème plus petit est que vous utilisez une variable locale
@BATCHSIZE
dans une expression TOP sans unRECOMPILE
ou 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:La partie clé est ici:
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@STARTID
variable avance après chaque boucle afin que vous puissiez éviter de lire la même ligne plus d'une fois dans la table source.la source
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).
la source