Share via


Résoudre les problèmes de performances UPDATE avec des plans étroits et larges dans SQL Server

S’applique à : SQL Server

Une UPDATE instruction peut être plus rapide dans certains cas et plus lente dans d’autres. De nombreux facteurs peuvent entraîner une telle variation, notamment le nombre de lignes mises à jour et l’utilisation des ressources sur le système (blocage, processeur, mémoire ou E/S). Cet article traite d’une raison spécifique de l’écart : le choix du plan de requête effectué par SQL Server.

Que sont les plans étroits et larges ?

Lorsque vous exécutez une UPDATE instruction sur une colonne d’index cluster, SQL Server met à jour non seulement l’index cluster lui-même, mais également tous les index non cluster, car les index non cluster contiennent la clé d’index de cluster.

SQL Server dispose de deux options pour effectuer la mise à jour :

  • Plan étroit : effectuez la mise à jour de l’index non cluster, ainsi que la mise à jour de la clé d’index cluster. Cette approche simple est facile à comprendre ; mettre à jour l’index cluster, puis mettre à jour tous les index non cluster en même temps. SQL Server mettre à jour une ligne et passer à la suivante jusqu’à ce que toutes les lignes soient terminées. Cette approche est appelée mise à jour de plan étroit ou mise à jour Per-Row. Toutefois, cette opération est relativement coûteuse, car l’ordre des données d’index non cluster qui seront mises à jour peut ne pas être dans l’ordre des données d’index cluster. Si de nombreuses pages d’index sont impliquées dans la mise à jour, lorsque les données sont sur le disque, un grand nombre de demandes d’E/S aléatoires peuvent se produire.

  • Plan large : pour optimiser les performances et réduire les E/S aléatoires, SQL Server pouvez choisir un plan large. Il n’effectue pas la mise à jour des index non cluster, ainsi que la mise à jour de l’index cluster. Au lieu de cela, il trie d’abord toutes les données d’index non cluster en mémoire, puis met à jour tous les index dans cet ordre. Cette approche est appelée plan large (également appelée mise à jour Per-Index).

Voici une capture d’écran des plans étroits et larges :

Capture d’écran des plans étroits et larges.

Quand SQL Server choisissez-vous un plan large ?

Deux critères doivent être remplis pour SQL Server choisir un plan large :

  • Le nombre de lignes affectées est supérieur à 250.
  • La taille du niveau feuille des index non cluster (nombre de pages d’index * 8 Ko) est d’au moins 1/1 000 du paramètre de mémoire maximale du serveur.

Comment fonctionnent les plans étroits et larges ?

Pour comprendre le fonctionnement des plans étroits et larges, procédez comme suit dans l’environnement suivant :

  • SQL Server 2019 CU11
  • Mémoire maximale du serveur = 1 500 Mo
  1. Exécutez le script suivant pour créer une table mytable1 contenant 41 501 lignes, un index cluster sur la colonne c1et cinq index non cluster sur le reste des colonnes, respectivement.

    CREATE TABLE mytable1(c1 INT,c2 CHAR(30),c3 CHAR(20),c4 CHAR(30),c5 CHAR(30))
    GO
    WITH cte
    AS
    (
      SELECT ROW_NUMBER() OVER(ORDER BY c1.object_id) id FROM sys.columns CROSS JOIN sys.columns c1
    )
    INSERT mytable1
    SELECT TOP 41000 id,REPLICATE('a',30),REPLICATE('a',20),REPLICATE('a',30),REPLICATE('a',30) 
    FROM cte
    GO
    
    INSERT mytable1
    SELECT TOP 250 50000,c2,c3,c4,c5 
    FROM mytable1
    GO
    
    INSERT mytable1
    SELECT TOP 251 50001,c2,c3,c4,c5 
    FROM mytable1
    GO
    
    CREATE CLUSTERED INDEX ic1 ON mytable1(c1)
    CREATE INDEX ic2 ON mytable1(c2)
    CREATE INDEX ic3 ON mytable1(c3)
    CREATE INDEX ic4 ON mytable1(c4)
    CREATE INDEX ic5 ON mytable1(c5)
    
  2. Exécutez les trois instructions T-SQL UPDATE suivantes et comparez les plans de requête :

    • UPDATE mytable1 SET c1=c1 WHERE c1=1 OPTION(RECOMPILE) - une ligne est mise à jour
    • UPDATE mytable1 SET c1=c1 WHERE c1=50000 OPTION(RECOMPILE) - 250 lignes sont mises à jour.
    • UPDATE mytable1 SET c1=c1 WHERE c1=50001 OPTION(RECOMPILE) - 251 lignes sont mises à jour.
  3. Examinez les résultats en fonction du premier critère (le seuil du nombre de lignes affecté est de 250).

    La capture d’écran suivante montre les résultats basés sur le premier critère :

    Capture d’écran des plans larges et étroits basés sur la taille de l’index.

    Comme prévu, l’optimiseur de requête choisit un plan étroit pour les deux premières requêtes, car le nombre de lignes affectées est inférieur à 250. Un plan large est utilisé pour la troisième requête, car le nombre de lignes affectées est de 251, ce qui est supérieur à 250.

  4. Examinez les résultats en fonction du deuxième critère (la mémoire de la taille de l’index feuille est au moins égale à 1/1000 du paramètre mémoire maximale du serveur).

    La capture d’écran suivante montre les résultats basés sur le deuxième critère :

    Capture d’écran du plan large n’utilisant pas d’index en raison de la taille.

    Un plan large est sélectionné pour la troisième UPDATE requête. Toutefois, l’index ic3 (sur la colonne c3) n’est pas visible dans le plan. Le problème se produit parce que le deuxième critère n’est pas rempli : la taille de l’index des pages feuilles par rapport au paramètre mémoire maximale du serveur.

    Le type de données de colonne , et est , tandis que le type de données de colonne c3 est char(20).char(30)c4c4c2 La taille de chaque ligne d’index ic3 étant inférieure à celle des autres, le nombre de pages feuilles est inférieur à celui des autres.

    Avec l’aide de la fonction de gestion dynamique (DMF), sys.dm_db_database_page_allocationsvous pouvez calculer le nombre de pages pour chaque index. Pour les ic2index , ic4et ic5, chaque index a 214 pages, dont 209 sont des pages feuilles (les résultats peuvent varier légèrement). La mémoire consommée par les pages feuilles est de 209 x 8 = 1 672 Ko. Par conséquent, le rapport est de 1672/(1500 x 1024) = 0,00108854101, ce qui est supérieur à 1/1000. Cependant, le ic3 seul compte 161 pages ; 159 d’entre elles sont des pages feuilles. Le rapport est de 159 x 8/(1500 x 1024) = 0,000828125, ce qui est inférieur à 1/1000 (0,001).

    Si vous insérez plus de lignes ou si vous réduisez la mémoire maximale du serveur pour répondre au critère, le plan change. Pour que la taille de niveau feuille de l’index soit supérieure à 1/1000, vous pouvez réduire le paramètre de mémoire maximale du serveur à 1 200 en exécutant les commandes suivantes :

    sp_configure 'show advanced options', 1;
    GO
    RECONFIGURE;
    GO
    sp_configure 'max server memory', 1200;
    GO
    RECONFIGURE
    GO
    UPDATE mytable1 SET c1=c1 WHERE c1=50001 OPTION(RECOMPILE) --251 rows are updated.
    

    Dans ce cas, 159 x 8/(1200 x 1024) = 0,00103515625 > 1/1000. Après cette modification, le ic3 apparaît dans le plan.

    Pour plus d’informations sur show advanced options, consultez Utiliser Transact-SQL.

    La capture d’écran suivante montre que le plan large utilise tous les index lorsque le seuil de mémoire est atteint :

    Capture d’écran du plan large qui utilise tous les index lorsque le seuil de mémoire est atteint.

Un plan large est-il plus rapide qu’un plan étroit ?

La réponse est que cela dépend de la mise en cache ou non des pages de données et d’index dans le pool de mémoires tampons.

Les données sont mises en cache dans le pool de mémoires tampons

Si les données se trouvent déjà dans le pool de mémoires tampons, la requête avec le plan large n’offre pas nécessairement d’avantages supplémentaires en matière de performances par rapport aux plans étroits, car le plan large est conçu pour améliorer les performances d’E/S (lectures physiques, et non lectures logiques).

Pour tester si un plan large est plus rapide qu’un plan étroit lorsque les données se situent dans un pool de mémoires tampons, procédez comme suit dans l’environnement suivant :

  • SQL Server 2019 CU11

  • Mémoire maximale du serveur : 30 000 Mo

  • La taille des données est de 64 Mo, tandis que la taille de l’index est d’environ 127 Mo.

  • Les fichiers de base de données se trouvent sur deux disques physiques différents :

    • I :\sql19\dbWideplan.mdf
    • H :\sql19\dbWideplan.ldf
  1. Créez une autre table, mytable2, en exécutant les commandes suivantes :

    CREATE TABLE mytable2(C1 INT,C2 INT,C3 INT,C4 INT,C5 INT)
    GO
    CREATE CLUSTERED INDEX IC1 ON mytable2(C1)
    CREATE INDEX IC2 ON mytable2(C2)
    CREATE INDEX IC3 ON mytable2(C3)
    CREATE INDEX IC4 ON mytable2(C4)
    CREATE INDEX IC5 ON mytable2(C5)
    GO
    DECLARE @N INT=1
    WHILE @N<1000000
    BEGIN
      DECLARE @N1 INT=RAND()*4500
      DECLARE @N2 INT=RAND()*100000
      DECLARE @N3 INT=RAND()*100000
      DECLARE @N4 INT=RAND()*100000
      DECLARE @N5 INT=RAND()*100000
      INSERT mytable2 VALUES(@N1,@N2,@N3,@N4,@N5)
      SET @N+=1
    END
    GO
    UPDATE STATISTICS mytable2 WITH FULLSCAN
    
  2. Exécutez les deux requêtes suivantes pour comparer les plans de requête :

    update mytable2 set c1=c1 where c2<260 option(querytraceon 8790) --trace flag 8790 will force Wide plan
    update mytable2 set c1=c1 where c2<260 option(querytraceon 2338) --trace flag 2338 will force Narrow plan
    

    Pour plus d’informations, consultez indicateur de trace 8790 et indicateur de trace 2338.

    La requête avec le plan large prend 0,136 seconde, tandis que la requête avec le plan étroit prend seulement 0,112 seconde. Les deux durées sont très proches et la mise à jour Per-Index (plan large) est moins bénéfique, car les données se trouvent déjà dans la mémoire tampon avant l’exécution de l’instruction UPDATE .

    La capture d’écran suivante montre des plans larges et étroits lorsque les données sont mises en cache dans le pool de mémoires tampons :

    Capture d’écran des plans larges et étroits lorsque les données sont mises en cache dans le pool de mémoires tampons.

Les données ne sont pas mises en cache dans le pool de mémoires tampons

Pour tester si un plan large est plus rapide qu’un plan étroit lorsque les données ne sont pas dans le pool de mémoires tampons, exécutez les requêtes suivantes :

Remarque

Lorsque vous effectuez le test, vérifiez que votre charge de travail est la seule dans SQL Server et que les disques sont dédiés à SQL Server.

CHECKPOINT
GO
DBCC DROPCLEANBUFFERS
GO
WAITFOR DELAY '00:00:02' --wait for 1~2 seconds
UPDATE mytable2 SET c1=c1 WHERE c2 < 260 OPTION (QUERYTRACEON 8790) --force Wide plan
CHECKPOINT 
GO
DBCC DROPCLEANBUFFERS
GO 
WAITFOR DELAY '00:00:02' --wait for 1~2 SECONDS
UPDATE mytable2 SET c1=c1 WHERE c2 < 260 OPTION (QUERYTRACEON 2338) --force Narrow plan

La requête avec un plan large prend 3,554 secondes, tandis que la requête avec un plan étroit prend 6,701 secondes. Cette fois, la requête de plan large s’exécute plus rapidement.

La capture d’écran suivante montre le plan large lorsque les données ne sont pas mises en cache dans le pool de mémoires tampons :

Capture d’écran du plan large lorsque les données ne sont pas mises en cache dans le pool de mémoires tampons.

La capture d’écran suivante montre le plan étroit lorsque les données ne sont pas mises en cache dans le pool de mémoires tampons :

Capture d’écran du plan étroit lorsque les données ne sont pas mises en cache dans le pool de mémoires tampons.

Une requête de plan large est-elle toujours plus rapide qu’un plan de requête étroit lorsque les données ne sont pas dans la mémoire tampon ?

La réponse est « pas toujours ». Pour tester si la requête de plan large est toujours plus rapide que le plan de requête étroit lorsque les données ne sont pas dans la mémoire tampon, procédez comme suit :

  1. Créez une autre table, mytable2, en exécutant les commandes suivantes :

    SELECT c1,c1 AS c2,c1 AS C3,c1 AS c4,c1 AS C5 INTO mytable3 FROM mytable2
    GO
    CREATE CLUSTERED INDEX IC1 ON mytable3(C1)
    CREATE INDEX IC2 ON mytable3(C2)
    CREATE INDEX IC3 ON mytable3(C3)
    CREATE INDEX IC4 ON mytable3(C4)
    CREATE INDEX IC5 ON mytable3(C5)
    GO
    

    est mytable3 identique à mytable2, à l’exception des données. mytable3 a les cinq colonnes avec la même valeur, ce qui fait que l’ordre des index non cluster suit l’ordre de l’index cluster. Ce tri des données réduit l’avantage du plan large.

  2. Exécutez les commandes suivantes pour comparer les plans de requête :

    CHECKPOINT 
    GO
    DBCC DROPCLEANBUFFERS
    go
    UPDATE mytable3 SET c1=c1 WHERE c2<12 OPTION(QUERYTRACEON 8790) --tf 8790 will force Wide plan
    
    CHECKPOINT 
    GO
    DBCC DROPCLEANBUFFERS
    GO
    UPDATE mytable3 SET c1=c1 WHERE c2<12 OPTION(QUERYTRACEON 2338) --tf 2338 will force Narrow plan
    

    La durée des deux requêtes est considérablement réduite ! Le plan large prend 0,304 seconde, ce qui est un peu plus lent que le plan étroit cette fois.

    La capture d’écran suivante montre la comparaison des performances lorsque large et étroite sont utilisées :

    Capture d’écran montrant la comparaison des performances lorsque large et étroite sont utilisées.

Scénarios où les plans larges sont appliqués

Voici les autres scénarios dans lesquels des plans larges sont également appliqués :

La colonne d’index cluster a une clé unique ou primaire, et plusieurs lignes sont mises à jour

Voici un exemple pour reproduire le scénario :

CREATE TABLE mytable4(c1 INT primary key,c2 INT,c3 INT,c4 INT)
GO
CREATE INDEX ic2 ON mytable4(c2)
CREATE INDEX ic3 ON mytable4(c3)
CREATE INDEX ic4 ON mytable4(c4)
GO
INSERT mytable4 VALUES(0,0,0,0)
INSERT mytable4 VALUES(1,1,1,1)

La capture d’écran suivante montre que le plan large est utilisé lorsque l’index de cluster a une clé unique :

Capture d’écran du plan large utilisé lorsque l’index de cluster a une clé unique.

Pour plus d’informations, consultez Maintenance des index uniques.

La colonne d’index de cluster est spécifiée dans le schéma de partition

Voici un exemple pour reproduire le scénario :

CREATE TABLE mytable5(c1 INT,c2 INT,c3 INT,c4 INT)
GO
IF EXISTS (SELECT * FROM sys.partition_schemes WHERE name='PS1')
    DROP PARTITION SCHEME PS1
GO
IF EXISTS (SELECT * FROM sys.partition_functions WHERE name='PF1')
    DROP PARTITION FUNCTION PF1
GO
CREATE PARTITION FUNCTION PF1(INT) AS 
  RANGE right FOR VALUES 
  (2000)   
GO
CREATE PARTITION SCHEME PS1 AS 
  PARTITION PF1 all TO 
  ([PRIMARY]) 
GO 
CREATE CLUSTERED INDEX c1 ON mytable5(c1) ON PS1(c1)
CREATE INDEX c2 ON mytable5(c2)
CREATE INDEX c3 ON mytable5(c3)
CREATE INDEX c4 ON mytable5(c4)
GO
UPDATE mytable5 SET c1=c1 WHERE c1=1 

La capture d’écran suivante montre que le plan large est utilisé lorsqu’il existe une colonne en cluster dans le schéma de partition :

Capture d’écran montrant que le plan large est utilisé lorsqu’il existe une colonne en cluster dans le schéma de partition.

La colonne d’index cluster ne fait pas partie du schéma de partition et la colonne du schéma de partition est mise à jour

Voici un exemple pour reproduire le scénario :

CREATE TABLE mytable6(c1 INT,c2 INT,c3 INT,c4 INT)
GO
IF EXISTS (SELECT * FROM sys.partition_schemes WHERE name='PS2')
    DROP PARTITION SCHEME PS2
GO
IF EXISTS (SELECT * FROM sys.partition_functions WHERE name='PF2')
    DROP PARTITION FUNCTION PF2
GO
CREATE PARTITION FUNCTION PF2(int) AS 
  RANGE right FOR VALUES 
  (2000)   
GO
CREATE PARTITION SCHEME PS2 AS 
  PARTITION PF2 all TO 
  ([PRIMARY]) 
GO 
CREATE CLUSTERED INDEX c1 ON mytable6(c1) ON PS2(c2) --on c2 column
CREATE INDEX c3 ON mytable6(c3)
CREATE INDEX c4 ON mytable6(c4)

La capture d’écran suivante montre que le plan large est utilisé lorsque la colonne du schéma de partition est mise à jour :

Capture d’écran du plan large utilisé lors de la mise à jour de la colonne de schéma de partition.

Conclusion

  • SQL Server choisit une mise à jour de plan étendue lorsque les critères suivants sont remplis en même temps :

    • Le nombre de lignes affectées est supérieur à 250.
    • La mémoire de l’index feuille est au moins égale à 1/000 du paramètre de mémoire maximale du serveur.
  • Les plans étendus augmentent les performances au détriment de la consommation de mémoire supplémentaire.

  • Si le plan de requête attendu n’est pas utilisé, cela peut être dû à des statistiques obsolètes (ne signalant pas la taille correcte des données), au paramètre de mémoire maximale du serveur ou à d’autres problèmes non liés tels que les plans sensibles aux paramètres.

  • La durée des UPDATE énoncés utilisant un plan large dépend de plusieurs facteurs et, dans certains cas, cela peut prendre plus de temps que des plans étroits.

  • L’indicateur de trace 8790 force un plan large ; l’indicateur de trace 2338 force un plan étroit.