Nous avons une très grande table (100 millions de lignes) et nous devons mettre à jour quelques champs dessus.
Pour l'envoi de grumes, etc., nous voulons aussi, évidemment, le garder pour des transactions de petite taille.
Le code est:
DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000
UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null
WHILE @@ROWCOUNT > 0
BEGIN
UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null
END
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é, je vais résoudre 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 (il s'agit de 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 de 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 sentez que la table d'état n'est jamais synchronisée en raison de l'annulation, il est facile de rafraîchir avec la valeur actuelle en la mettant simplement à jour manuellement avec la fonction et il y a deux tables à METTRE À JOUR (c.-à-d. la table principale et la table d'état), nous devrait utiliser une transaction explicite pour garder ces deux tables synchronisées, mais nous ne voulons pas risquer d'avoir un transaction orpheline si vous annulez le processus à un moment donné après qu'il a commencé la transaction mais ne l'a pas validée. Cela devrait être sûr à faire tant que vous n'arrêtez pas le travail de l'Agent SQL.COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL
.
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 moment opportun (c'est-à-dire quand il n'y a pas de transaction active!) Et le faire se nettoyer tout Nice et bien rangé.
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 devrions également vérifier au tout début, car il est inutile de remplir une table temporaire avec 1 million de lignes si le processus est en cours 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 souhaitez 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;
MISE À 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/la possibilité de tester certaines variantes ;-).
TOP (@UpdateRows)
à la requête UPDATE pour que la ligne du haut ressemble à:UPDATE TOP (@UpdateRows) ht
Ajoutez une CLÉ PRIMAIRE à la table temporaire #CurrentSet
. L'idée ici est d'aider l'optimiseur avec le JOIN à la table de 100 millions de lignes.
Et juste pour qu'il soit déclaré afin de ne pas être ambigu, il ne devrait y avoir aucune raison d'ajouter un PK à la table temporaire #FullSet
Car il s'agit simplement d'une table de file d'attente simple où la commande n'est pas pertinente.
SELECT
qui alimente la table temporaire #FullSet
. Voici quelques considérations relatives à l'ajout d'un tel index: WHERE deleted is null or deletedDate is null
SELECT
, endommagera le UPDATE
car c'est 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 de s'engager les E/S supplémentaires).Répondre à la deuxième partie: comment imprimer une sortie pendant la boucle.
J'ai quelques procédures de maintenance de longue durée que l'administrateur système doit parfois exécuter.
Je les exécute à partir de SSMS et j'ai également remarqué que l'instruction PRINT
n'est affichée dans SSMS qu'après la fin de la procédure.
Donc, j'utilise RAISERROR
avec une faible gravité:
DECLARE @VarTemp nvarchar(32);
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
J'utilise SQL Server 2008 Standard et SSMS 2012 (11.0.3128.0). Voici un exemple de travail complet à exécuter dans SSMS:
DECLARE @VarCount int = 0;
DECLARE @VarTemp nvarchar(32);
WHILE @VarCount < 3
BEGIN
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
--RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
--PRINT @VarTemp;
WAITFOR DELAY '00:00:02';
SET @VarCount = @VarCount + 1;
END
Lorsque je commente RAISERROR
et que je ne laisse que PRINT
, les messages de l'onglet Messages dans SSMS n'apparaissent qu'après la fin du lot, au bout de 6 secondes.
Lorsque je commente PRINT
et que j'utilise RAISERROR
, les messages de l'onglet Messages dans SSMS apparaissent sans attendre 6 secondes, mais à mesure que la boucle progresse.
Fait intéressant, lorsque j'utilise à la fois RAISERROR
et PRINT
, je vois les deux messages. Le premier message vient du premier RAISERROR
, puis du retard de 2 secondes, puis du premier PRINT
et du deuxième RAISERROR
, etc.
Dans d'autres cas, j'utilise une table log
dédiée et j'insère simplement une ligne dans la table avec des informations décrivant l'état actuel et l'horodatage du processus de longue durée.
Pendant que le long processus s'exécute, je périodiquement SELECT
à partir de la table log
pour voir ce qui se passe.
Cela a évidemment certains frais généraux, mais il laisse un journal (ou historique des journaux) que je peux examiner à mon propre rythme plus tard.
Vous pouvez le surveiller à partir d'une autre connexion avec quelque chose comme:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(*) FROM [huge-table] WHERE deleted IS NULL OR deletedDate IS NULL
pour voir combien il reste à faire. Cela peut être utile si une application appelle le processus, plutôt que si vous l'exécutez manuellement dans SSMS ou similaire, et doit afficher la progression: exécutez le processus principal de manière asynchrone (ou sur un autre thread), puis bouclez en appelant le "combien il reste "vérifier de temps en temps jusqu'à ce que l'appel asynchrone (ou thread) se termine.
La définition du niveau d'isolement le plus laxiste possible signifie que cela devrait revenir dans un délai raisonnable sans être bloqué derrière la transaction principale en raison de problèmes de verrouillage. Cela pourrait signifier que la valeur retournée est un peu inexacte bien sûr, mais en tant que simple indicateur de progression, cela ne devrait pas avoir d'importance du tout.