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 lignes MySourceTable
et effectue 120000 recherches par rapport à la vendor
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 depuis MySourceTable
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 lues MySourceTable
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 un RECOMPILE
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:
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 @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.