web-dev-qa-db-fra.com

serveur SQL: mise à jour des champs sur une table énorme en petits morceaux: comment obtenir la progression / le statut?

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.

  • Est-ce que ce qui suit fera l'affaire?
  • Et comment pouvons-nous lui faire imprimer une sortie, afin que nous puissions voir les progrès? (nous avons essayé d'ajouter une instruction PRINT là-dedans, mais rien n'a été émis pendant la boucle while)

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
10

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 COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL. 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.

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

  1. 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.
  2. 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.

  3. Dans certains cas, il est utile d'ajouter un index filtré pour aider la SELECT qui alimente la table temporaire #FullSet. Voici quelques considérations relatives à l'ajout d'un tel index:
    1. La condition WHERE doit correspondre à la condition WHERE de votre requête, d'où WHERE deleted is null or deletedDate is null
    2. 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.
    3. 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
    4. N'oubliez pas que l'index, tout en aidant le 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).
12
Solomon Rutzky

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.

4
Vladimir Baranov

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.

2
David Spillett