Je n'étais pas au courant de cette question lorsque j'ai répondu à la question connexe ( des transactions explicites sont-elles nécessaires dans cette boucle while? ), Mais par souci d'exhaustivité, j'aborderai ce problème ici car il ne faisait pas partie de ma suggestion dans cette réponse liée .
Étant donné que je suggère de planifier cela via un travail SQL Agent (c'est 100 millions de lignes, après tout), je ne pense pas que toute forme d'envoi de messages d'état au client (c'est-à-dire SSMS) soit idéale (bien que si c'est le cas jamais besoin d'autres projets, alors je suis d'accord avec Vladimir que l'utilisation RAISERROR('', 10, 1) WITH NOWAIT;
est la voie à suivre).
Dans ce cas particulier, je créerais une table d'état qui peut être mise à jour pour chaque boucle avec le nombre de lignes mis à jour jusqu'à présent. Et cela ne fait pas de mal de jeter l'heure actuelle pour avoir un rythme cardiaque sur le processus.
Étant donné que vous souhaitez pouvoir annuler et redémarrer le processus, Je suis las d'envelopper la mise à jour de la table principale avec la mise à jour de la table d'état dans une transaction explicite. Cependant, si vous pensez que la table d'état est toujours désynchronisée en raison de l'annulation, il est facile de rafraîchir avec la valeur actuelle en la mettant simplement à jour manuellement avec le COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL
.et il y a deux tables à METTRE À JOUR (c'est-à-dire la table principale et la table d'état), nous devrions utiliser une transaction explicite pour garder ces deux tables synchronisées, mais nous ne voulons pas risquer d'avoir une transaction orpheline si vous annulez le processus à un après avoir démarré la transaction mais ne l'a pas validée. Cela devrait être sûr tant que vous n'arrêtez pas le travail de l'Agent SQL.
Comment pouvez-vous arrêter le processus sans, euh, bien, l'arrêter? En lui demandant d'arrêter :-). Oui. En envoyant au processus un "signal" (similaire à kill -3
sous Unix), vous pouvez demander qu'il s'arrête au prochain moment opportun (c'est-à-dire lorsqu'il n'y a pas de transaction active!) Et qu'il se nettoie de manière agréable et ordonnée.
Comment pouvez-vous communiquer avec le processus en cours dans une autre session? En utilisant le même mécanisme que nous avons créé pour qu'il vous communique son état actuel: la table d'état. Nous avons juste besoin d'ajouter une colonne que le processus vérifiera au début de chaque boucle afin qu'il sache s'il faut continuer ou abandonner. Et puisque l'intention est de planifier cela en tant que travail de l'Agent SQL (exécuté toutes les 10 ou 20 minutes), nous devons également vérifier au tout début, car il est inutile de remplir une table temporaire avec 1 million de lignes si le processus se poursuit pour quitter un instant plus tard et ne pas utiliser ces données.
DECLARE @BatchRows INT = 1000000,
@UpdateRows INT = 4995;
IF (OBJECT_ID(N'dbo.HugeTable_TempStatus') IS NULL)
BEGIN
CREATE TABLE dbo.HugeTable_TempStatus
(
RowsUpdated INT NOT NULL, -- updated by the process
LastUpdatedOn DATETIME NOT NULL, -- updated by the process
PauseProcess BIT NOT NULL -- read by the process
);
INSERT INTO dbo.HugeTable_TempStatus (RowsUpdated, LastUpdatedOn, PauseProcess)
VALUES (0, GETDATE(), 0);
END;
-- First check to see if we should run. If no, don't waste time filling temp table
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
PRINT 'Process is paused. No need to start.';
RETURN;
END;
CREATE TABLE #FullSet (KeyField1 DataType1, KeyField2 DataType2);
CREATE TABLE #CurrentSet (KeyField1 DataType1, KeyField2 DataType2);
INSERT INTO #FullSet (KeyField1, KeyField2)
SELECT TOP (@BatchRows) ht.KeyField1, ht.KeyField2
FROM dbo.HugeTable ht
WHERE ht.deleted IS NULL
OR ht.deletedDate IS NULL
WHILE (1 = 1)
BEGIN
-- Check if process is paused. If yes, just exit cleanly.
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
PRINT 'Process is paused. Exiting.';
BREAK;
END;
-- grab a set of rows to update
DELETE TOP (@UpdateRows)
FROM #FullSet
OUTPUT Deleted.KeyField1, Deleted.KeyField2
INTO #CurrentSet (KeyField1, KeyField2);
IF (@@ROWCOUNT = 0)
BEGIN
RAISERROR(N'All rows have been updated!!', 16, 1);
BREAK;
END;
BEGIN TRY
BEGIN TRAN;
-- do the update of the main table
UPDATE ht
SET ht.deleted = 0,
ht.deletedDate = '2000-01-01'
FROM dbo.HugeTable ht
INNER JOIN #CurrentSet cs
ON cs.KeyField1 = ht.KeyField1
AND cs.KeyField2 = ht.KeyField2;
-- update the current status
UPDATE ts
SET ts.RowsUpdated += @@ROWCOUNT,
ts.LastUpdatedOn = GETDATE()
FROM dbo.HugeTable_TempStatus ts;
COMMIT TRAN;
END TRY
BEGIN CATCH
IF (@@TRANCOUNT > 0)
BEGIN
ROLLBACK TRAN;
END;
THROW; -- raise the error and terminate the process
END CATCH;
-- clear out rows to update for next iteration
TRUNCATE TABLE #CurrentSet;
WAITFOR DELAY '00:00:01'; -- 1 second delay for some breathing room
END;
-- clean up temp tables when testing
-- DROP TABLE #FullSet;
-- DROP TABLE #CurrentSet;
Vous pouvez ensuite vérifier l'état à tout moment à l'aide de la requête suivante:
SELECT sp.[rows] AS [TotalRowsInTable],
ts.RowsUpdated,
(sp.[rows] - ts.RowsUpdated) AS [RowsRemaining],
ts.LastUpdatedOn
FROM sys.partitions sp
CROSS JOIN dbo.HugeTable_TempStatus ts
WHERE sp.[object_id] = OBJECT_ID(N'ResizeTest')
AND sp.[index_id] < 2;
Vous voulez suspendre le processus, qu'il s'exécute dans un travail SQL Agent ou même dans SSMS sur l'ordinateur de quelqu'un d'autre? Exécutez simplement:
UPDATE ht
SET ht.PauseProcess = 1
FROM dbo.HugeTable_TempStatus ts;
Vous voulez que le processus puisse recommencer? Exécutez simplement:
UPDATE ht
SET ht.PauseProcess = 0
FROM dbo.HugeTable_TempStatus ts;
METTRE À JOUR:
Voici quelques éléments supplémentaires à essayer qui pourraient améliorer les performances de cette opération. Aucun n'est garanti pour aider, mais vaut probablement la peine d'être testé. Et avec 100 millions de lignes à mettre à jour, vous avez amplement le temps / l'opportunité de tester certaines variantes ;-).
- Ajoutez
TOP (@UpdateRows)
à la requête UPDATE pour que la ligne du haut ressemble à:
UPDATE TOP (@UpdateRows) ht
Parfois, cela aide l'optimiseur à savoir combien de lignes max seront affectées afin de ne pas perdre de temps à en chercher plus.
Ajoutez une CLÉ PRIMAIRE à la #CurrentSet
table temporaire. L'idée ici est d'aider l'optimiseur avec le JOIN à la table de 100 millions de lignes.
Et juste pour l'avoir déclaré afin de ne pas être ambigu, il ne devrait pas y avoir de raison d'ajouter un PK à la #FullSet
table temporaire car c'est juste une simple table de file d'attente où la commande n'est pas pertinente.
- Dans certains cas, il est utile d'ajouter un index filtré pour aider celui
SELECT
qui alimente la #FullSet
table temporaire. Voici quelques considérations liées à l'ajout d'un tel index:
- La condition WHERE doit correspondre à la condition WHERE de votre requête, d'où
WHERE deleted is null or deletedDate is null
- Au début du processus, la plupart des lignes correspondront à votre condition WHERE, donc un index n'est pas très utile. Vous voudrez peut-être attendre quelque part autour de la barre des 50% avant d'ajouter ceci. Bien sûr, combien cela aide et quand il est préférable d'ajouter l'indice varie en raison de plusieurs facteurs, c'est donc un peu d'essai et d'erreur.
- Vous devrez peut-être mettre à jour manuellement les statistiques et / ou reconstruire l'index pour le maintenir à jour car les données de base changent assez fréquemment
- Assurez-vous de garder à l'esprit que l'index, tout en aidant le
SELECT
, nuira au UPDATE
car il s'agit d'un autre objet qui doit être mis à jour pendant cette opération, donc plus d'E / S. Cela joue à la fois en utilisant un index filtré (qui rétrécit à mesure que vous mettez à jour les lignes car moins de lignes correspondent au filtre), et en attendant un peu pour ajouter l'index (si cela ne sera pas très utile au début, alors aucune raison d'engager les E / S supplémentaires).