Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Comment mettre à jour une grande table avec des millions de lignes dans SQL Server ?

  1. Vous ne devez pas mettre à jour 10 000 lignes dans un ensemble, sauf si vous êtes certain que l'opération obtient des verrous de page (car plusieurs lignes par page font partie de la UPDATE opération). Le problème est que l'escalade de verrous (des verrous de ligne ou de page vers le tableau) se produit à 5 000 verrous . Il est donc plus sûr de le maintenir juste en dessous de 5000, juste au cas où l'opération utilise des verrous de ligne.

  2. Vous ne devriez pas utilisez SET ROWCOUNT pour limiter le nombre de lignes qui seront modifiées. Il y a deux problèmes ici :

    1. Il est obsolète depuis la sortie de SQL Server 2005 (il y a 11 ans) :

      L'utilisation de SET ROWCOUNT n'affectera pas les instructions DELETE, INSERT et UPDATE dans une future version de SQL Server. Évitez d'utiliser SET ROWCOUNT avec les instructions DELETE, INSERT et UPDATE dans les nouveaux travaux de développement et prévoyez de modifier les applications qui l'utilisent actuellement. Pour un comportement similaire, utilisez la syntaxe TOP

    2. Cela peut affecter plus que la seule déclaration à laquelle vous avez affaire :

      La définition de l'option SET ROWCOUNT entraîne l'arrêt du traitement de la plupart des instructions Transact-SQL lorsqu'elles ont été affectées par le nombre de lignes spécifié. Cela inclut les déclencheurs. L'option ROWCOUNT n'affecte pas les curseurs dynamiques, mais elle limite le jeu de lignes de jeu de clés et les curseurs insensibles. Cette option doit être utilisée avec prudence.

    Utilisez plutôt le TOP () clause.

  3. Il n'y a aucun but à avoir une transaction explicite ici. Cela complique le code et vous n'avez aucune manipulation pour un ROLLBACK , ce qui n'est même pas nécessaire puisque chaque instruction est sa propre transaction (c'est-à-dire une validation automatique).

  4. En supposant que vous trouviez une raison de conserver la transaction explicite, vous n'avez pas de TRY / CATCH structure. Veuillez consulter ma réponse sur DBA.StackExchange pour un TRY / CATCH modèle qui gère les transactions :

    Sommes-nous obligés de gérer Transaction en code C# ainsi qu'en procédure Store

Je soupçonne que le vrai WHERE la clause n'est pas affichée dans l'exemple de code de la question, donc simplement en s'appuyant sur ce qui a été montré, c'est mieux modèle serait :

DECLARE @Rows INT,
        @BatchSize INT; -- keep below 5000 to be safe
    
SET @BatchSize = 2000;

SET @Rows = @BatchSize; -- initialize just to enter the loop

BEGIN TRY    
  WHILE (@Rows = @BatchSize)
  BEGIN
      UPDATE TOP (@BatchSize) tab
      SET    tab.Value = 'abc1'
      FROM  TableName tab
      WHERE tab.Parameter1 = 'abc'
      AND   tab.Parameter2 = 123
      AND   tab.Value <> 'abc1' COLLATE Latin1_General_100_BIN2;
      -- Use a binary Collation (ending in _BIN2, not _BIN) to make sure
      -- that you don't skip differences that compare the same due to
      -- insensitivity of case, accent, etc, or linguistic equivalence.

      SET @Rows = @@ROWCOUNT;
  END;
END TRY
BEGIN CATCH
  RAISERROR(stuff);
  RETURN;
END CATCH;

En testant @Rows contre @BatchSize , vous pouvez éviter cette dernière UPDATE requête (dans la plupart des cas) car le jeu final est généralement un certain nombre de lignes inférieur à @BatchSize , auquel cas nous savons qu'il n'y a plus rien à traiter (c'est ce que vous voyez dans la sortie indiquée dans votre réponse). Uniquement dans les cas où l'ensemble final de lignes est égal à @BatchSize ce code exécutera-t-il une dernière UPDATE affectant 0 lignes.

J'ai également ajouté une condition au WHERE clause pour empêcher les lignes qui ont déjà été mises à jour d'être mises à jour à nouveau.

REMARQUE CONCERNANT LES PERFORMANCES

J'ai souligné "mieux" ci-dessus (comme dans "c'est un meilleur model") car cela a plusieurs améliorations par rapport au code original de l'OP, et fonctionne bien dans de nombreux cas, mais n'est pas parfait pour tous les cas. Pour les tables d'au moins une certaine taille (qui varie en raison de plusieurs facteurs, je peux donc ' Pour être plus précis), les performances se dégraderont car il y aura moins de lignes à corriger si :

  1. il n'y a pas d'index pour prendre en charge la requête, ou
  2. il y a un index, mais au moins une colonne dans WHERE clause est un type de données chaîne qui n'utilise pas de classement binaire, d'où un COLLATE La clause est ajoutée à la requête ici pour forcer le classement binaire, et cela invalide l'index (pour cette requête particulière).

C'est la situation que @mikesigs a rencontrée, nécessitant donc une approche différente. La méthode mise à jour copie les ID de toutes les lignes à mettre à jour dans une table temporaire, puis utilise cette table temporaire pour INNER JOIN à la table en cours de mise à jour sur la ou les colonnes de clé d'index cluster. (Il est important de capturer et de joindre sur l'index cluster colonnes, qu'il s'agisse ou non de colonnes de clé primaire !).

Veuillez consulter la réponse de @mikesigs ci-dessous pour plus de détails. L'approche montrée dans cette réponse est un modèle très efficace que j'ai moi-même utilisé à de nombreuses reprises. Les seuls changements que je ferais sont :

  1. Créer explicitement le #targetIds table plutôt que d'utiliser SELECT INTO...
  2. Pour les #targetIds table, déclarez une clé primaire clusterisée sur la ou les colonnes.
  3. Pour les #batchIds table, déclarez une clé primaire clusterisée sur la ou les colonnes.
  4. Pour insertion dans #targetIds , utilisez INSERT INTO #targetIds (column_name(s)) SELECT et supprimer le ORDER BY car c'est inutile.

Donc, si vous n'avez pas d'index pouvant être utilisé pour cette opération et que vous ne pouvez pas en créer temporairement un qui fonctionnera réellement (un index filtré peut fonctionner, selon votre WHERE clause pour la UPDATE requête), puis essayez l'approche indiquée dans la réponse de @mikesigs (et si vous utilisez cette solution, veuillez voter pour).