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

10

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
Jonesome Reinstate Monica
la source

Réponses:

12

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

  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 #CurrentSettable 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 #FullSettable temporaire car c'est juste une simple table de file d'attente où la commande n'est pas pertinente.

  3. Dans certains cas, il est utile d'ajouter un index filtré pour aider celui SELECTqui alimente la #FullSettable temporaire. Voici quelques considérations liées à 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. Assurez-vous de garder à l'esprit que l'index, tout en aidant le SELECT, nuira au UPDATEcar 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).
Solomon Rutzky
la source
1
C'est excellent. Je le lance maintenant, et il fume que nous pouvons le faire fonctionner en ligne, pendant la journée. Je vous remercie!
Jonesome Reinstate Monica
@samsmith Veuillez consulter la section MISE À JOUR que je viens d'ajouter car il existe des idées pour rendre le processus encore plus rapide.
Solomon Rutzky
Sans les améliorations UPDATE, nous obtenons environ 8 millions de mises à jour / heure ... avec @BatchRows réglé sur 10000000 (dix millions)
Jonesome Reinstate Monica
@samsmith C'est super :) non? Gardez à l' esprit deux choses: 1) Le processus va ralentir car il y a de moins en moins de lignes correspondant à la clause WHERE, donc pourquoi il serait un bon moment pour ajouter un index filtré, mais vous avez déjà ajouté un indice de non filtré au commencez donc je ne sais pas si cela va aider ou nuire, mais je m'attends quand même à ce que le débit diminue à mesure qu'il se rapproche de la fin, et 2) vous pouvez augmenter le débit en réduisant le WAITFOR DELAYà une demi-seconde environ, mais c'est un compromis avec la concurrence et peut-être le montant envoyé via l'envoi de journaux.
Solomon Rutzky
Nous sommes satisfaits de 8 millions de lignes / heure. Oui, nous pouvons le voir ralentir. Nous hésitons à créer d'autres index (car la table est verrouillée pour toute la génération). Ce que nous avons fait plusieurs fois, c'est faire une réorganisation sur l'index existant (parce que c'est en ligne).
Jonesome Reinstate Monica
4

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' PRINTinstruction n'est affichée dans SSMS qu'après la fin de la procédure.

Donc, j'utilise RAISERRORavec 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 RAISERRORet ne laisse que PRINTles messages dans l'onglet Messages de SSMS, ils n'apparaissent qu'après la fin du lot, au bout de 6 secondes.

Lorsque je commente PRINTet utilise RAISERRORles messages de l'onglet Messages de SSMS, ils apparaissent sans attendre 6 secondes, mais au fur et à mesure que la boucle progresse.

Fait intéressant, lorsque j'utilise les deux RAISERRORet PRINT, je vois les deux messages. D'abord vient le message du premier RAISERROR, puis attendez 2 secondes, puis le premier PRINTet le deuxième RAISERROR, et ainsi de suite.


Dans d'autres cas, j'utilise une logtable dédiée distincte 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 SELECTde la logtable 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.

Vladimir Baranov
la source
Sur SQL 2008/2014, nous ne pouvons pas voir les résultats de raiseerror .... qu'est-ce qui nous manque?
Jonesome Reinstate Monica
@samsmith, j'ai ajouté un exemple complet. Essayez-le. Quel comportement obtenez-vous dans cet exemple simple?
Vladimir Baranov,
2

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.

David Spillett
la source