Des transactions explicites sont-elles nécessaires dans cette boucle while?

11

SQL Server 2014:

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.

Si nous laissons ce qui suit s'exécuter un peu, puis annulons / terminons la requête, le travail effectué jusqu'à présent sera-t-il entièrement validé, ou devons-nous ajouter des instructions BEGIN TRANSACTION / END TRANSACTION explicites afin de pouvoir annuler à tout moment?

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:

13

Les relevés individuels - DML, DDL, etc. - sont des transactions en soi. Donc oui, après chaque itération de la boucle (techniquement: après chaque instruction), tout ce que cette UPDATEinstruction a changé a été automatiquement validé.

Bien sûr, il y a toujours une exception, non? Il est possible d'activer les transactions implicites via SET IMPLICIT_TRANSACTIONS , auquel cas la première UPDATEinstruction démarrerait une transaction que vous auriez dû COMMITou ROLLBACKà la fin. Il s'agit d'un paramètre de niveau de session qui est désactivé par défaut dans la plupart des cas.

devons-nous ajouter des instructions BEGIN TRANSACTION / END TRANSACTION explicites afin de pouvoir annuler à tout moment?

Et en fait, étant donné que vous voulez pouvoir arrêter le processus et redémarrer, l'ajout d'une transaction explicite (ou l'activation des transactions implicites) serait une mauvaise idée car l'arrêt du processus pourrait l'attraper avant qu'il ne le fasse COMMIT. Dans ce cas, vous devrez émettre manuellement le COMMIT(si vous êtes dans SSMS), ou si vous l'exécutez à partir d'un travail de l'Agent SQL, vous n'avez pas cette opportunité et vous pourriez vous retrouver avec une transaction orpheline.


En outre, vous souhaiterez peut-être définir @CHUNK_SIZEun nombre plus petit. L'escalade de verrous se produit généralement à 5000 verrous acquis sur un seul objet. Selon la taille des lignes et s'il fait des verrous de ligne vs des verrous de page, vous pourriez dépasser cette limite. Si la taille d'une ligne est telle que seulement 1 ou 2 lignes tiennent par chaque page, alors vous pourriez toujours la voir même si elle fait des verrous de page.

Si la table est partitionnée, vous avez la possibilité de définir l' LOCK_ESCALATIONoption (introduite dans SQL Server 2008) pour la table AUTOafin qu'elle ne verrouille que la partition et non la table entière lors de l'escalade. Ou, pour n'importe quelle table, vous pouvez définir cette même option DISABLE, bien que vous deviez être très prudent à ce sujet. Voir ALTER TABLE pour plus de détails.

Voici une documentation qui parle de Lock Escalation et des seuils: Lock Escalation (il dit que cela s'applique à "SQL Server 2008 R2 et versions supérieures"). Et voici un article de blog qui traite de la détection et de la correction de l'escalade de verrous: Verrouillage dans Microsoft SQL Server (Partie 12 - Escalade de verrous) .


Sans rapport avec la question exacte, mais liée à la requête dans la question, il y a quelques améliorations qui pourraient être apportées ici (ou du moins il semble que ce soit juste en le regardant):

  1. Pour votre boucle, faire WHILE (@@ROWCOUNT = @CHUNK_SIZE)est légèrement mieux car si le nombre de lignes mises à jour lors de la dernière itération est inférieur au montant demandé pour UPDATE, alors il n'y a plus de travail à faire.

  2. Si le deletedchamp est un BITtype de données, alors n'est pas que la valeur déterminée par si oui ou non deletedDateest 2000-01-01? Pourquoi avez-vous besoin des deux?

  3. Si ces deux champs sont nouveaux et que vous les avez ajoutés afin que NULLcela puisse être une opération en ligne / non bloquante et que vous vouliez maintenant les mettre à jour à leur valeur "par défaut", cela n'était pas nécessaire. À partir de SQL Server 2012 (Enterprise Edition uniquement), l'ajout de NOT NULLcolonnes qui ont une contrainte DEFAULT sont des opérations non bloquantes tant que la valeur de DEFAULT est une constante. Donc, si vous n'utilisez pas encore les champs, déposez et rajoutez au fur NOT NULLet à mesure avec une contrainte DEFAULT.

  4. Si aucun autre processus ne met à jour ces champs pendant que vous effectuez cette MISE À JOUR, il serait plus rapide si vous mettiez en file d'attente les enregistrements que vous vouliez mettre à jour, puis travailliez simplement hors de cette file d'attente. Il y a un impact sur les performances dans la méthode actuelle car vous devez à nouveau interroger la table à chaque fois pour obtenir l'ensemble qui doit être modifié. Au lieu de cela, vous pouvez effectuer les opérations suivantes, qui ne balaient la table qu'une seule fois sur ces deux champs, puis émettent uniquement des instructions UPDATE très ciblées. Il n'y a également aucune pénalité à arrêter le processus à tout moment et à le démarrer plus tard, car la population initiale de la file d'attente trouvera simplement les enregistrements restants à mettre à jour.

    1. Créez une table temporaire (#FullSet) contenant uniquement les champs clés de l'index cluster.
    2. Créez une deuxième table temporaire (#CurrentSet) de cette même structure.
    3. insérer dans #FullSet via SELECT TOP(n) KeyField1, KeyField2 FROM [huge-table] where deleted is null or deletedDate is null;

      Le TOP(n)est là en raison de la taille de la table. Avec 100 millions de lignes dans la table, vous n'avez pas vraiment besoin de remplir la table de file d'attente avec cet ensemble de clés, surtout si vous prévoyez d'arrêter le processus de temps en temps et de le redémarrer plus tard. Alors peut-être fixé nà 1 million et laissez-le se terminer. Vous pouvez toujours planifier cela dans un travail de l'Agent SQL qui exécute l'ensemble de 1 million (ou peut-être même moins), puis attend que la prochaine heure planifiée reprenne. Vous pouvez ensuite planifier une exécution toutes les 20 minutes afin qu'il y ait une certaine marge de manœuvre forcée entre les séries de n, mais cela terminera tout le processus sans surveillance. Il suffit ensuite que le travail se supprime lorsqu'il n'y a plus rien à faire :-).

    4. en boucle, faites:
      1. Remplissez le lot actuel via quelque chose comme DELETE TOP (4995) FROM #FullSet OUTPUT Deleted.KeyField INTO #CurrentSet (KeyField);
      2. IF (@@ROWCOUNT = 0) BREAK;
      3. Faites la MISE À JOUR en utilisant quelque chose comme: UPDATE ht SET ht.deleted = 0, ht.deletedDate='2000-01-01' FROM [huge-table] ht INNER JOIN #CurrentSet cs ON cs.KeyField = ht.KeyField;
      4. Effacez l'ensemble actuel: TRUNCATE TABLE #CurrentSet;
  5. 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).

MISE À JOUR: Veuillez voir ma réponse à une question liée à cette question pour la mise en œuvre complète de ce qui est suggéré ci-dessus, y compris un mécanisme pour suivre l'état et annuler proprement: serveur sql: mise à jour des champs sur une grande table en petits morceaux: comment obtenir Le statut de la progression?

Solomon Rutzky
la source
Vos suggestions dans # 4 peuvent être plus rapides dans certains cas, mais cela semble être une complexité de code importante à ajouter. Je préférerais commencer simplement, et si cela ne répond pas à vos besoins, envisagez des alternatives.
Bacon Bits
@BaconBits A accepté de commencer simple. Pour être juste, ces suggestions n'étaient pas censées s'appliquer à tous les scénarios. La question concerne le traitement d'un très grand tableau (100 millions + lignes).
Solomon Rutzky