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

Impact du plan d'exécution sur les attentes ASYNC_NETWORK_IO - Partie 1

Il y a quelques semaines, une question intéressante a été posée sur la balise de hachage #SQLHelp sur Twitter concernant l'impact des plans d'exécution sur le type d'attente ASYNC_NETWORK_IO, et elle a généré des opinions divergentes et de nombreuses discussions intéressantes.

https://twitter.com/shawndube/status/1225476846537650176

Ma réponse immédiate à cela serait que quelqu'un interprète mal la cause et l'effet de cela, puisque le type d'attente ASYNC_NETWORK_IO est rencontré lorsque le moteur a des résultats à envoyer via TDS au client mais qu'il n'y a pas de tampons TDS disponibles sur la connexion pour les envoyer sur. D'une manière générale, cela signifie que le côté client ne consomme pas les résultats de manière efficace, mais sur la base de la discussion qui a suivi, je suis devenu suffisamment intrigué pour tester si un plan d'exécution aurait ou non un impact significatif sur les attentes ASYNC_NETWORK_IO.

Pour résumer :se concentrer sur ASYNC_NETWORK_IO attend seul comme métrique de réglage est une erreur. Plus une requête s'exécute rapidement, plus ce type d'attente s'accumulera probablement, même si le client consomme les résultats aussi rapidement que possible. (Voir également le récent article de Greg sur le fait de se concentrer uniquement sur les temps d'attente en général.)

Tester la configuration

Pour exécuter les tests pour cela, une table très simple a été générée sur la base d'un exemple qui m'a été fourni par e-mail par un autre membre de la communauté, qui a démontré un changement dans le type d'attente, mais avait également une requête entièrement différente entre les deux tests avec une table supplémentaire utilisée dans le deuxième test, et il y avait un commentaire pour désactiver les résultats, ce qui supprime la partie importante de ce type d'attente pour commencer, donc ce n'est pas seulement un changement de plan seul.

Remarque :Je voudrais souligner qu'il ne s'agit pas du tout d'une déclaration négative envers qui que ce soit ; la discussion qui a suivi et les tests supplémentaires issus de la reproduction originale fournie ont été très instructifs et ont conduit à des recherches supplémentaires pour comprendre ce type d'attente dans son ensemble. La reproduction originale a démontré une différence, mais avec des modifications supplémentaires qui ne faisaient pas partie de la question initiale telle que posée.

DROP TABLE IF EXISTS [DemoTable];
 
CREATE TABLE [DemoTable] (
  ID INT PRIMARY KEY,
  FILLER VARCHAR(100)
);
 
INSERT INTO [DemoTable] WITH (TABLOCK)
SELECT TOP (250000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 50)
  FROM master..spt_values t1
  CROSS JOIN master..spt_values t2
  CROSS JOIN master..spt_values t3
  OPTION (MAXDOP 1);
GO

En utilisant cette table comme ensemble de données de base pour tester différentes formes de plan à l'aide d'astuces, les requêtes suivantes ont été utilisées :

SELECT t1.ID, t2.FILLER, t2.FILLER
  FROM [DemoTable] t1
  INNER HASH JOIN [DemoTable] t2 ON t1.ID = t2.ID;
 
  SELECT t1.ID, t2.FILLER, t2.FILLER
  FROM [DemoTable] t1
  INNER MERGE JOIN [DemoTable] t2 ON t1.ID = t2.ID;
 
  SELECT t1.ID, t2.FILLER, t2.FILLER
  FROM [DemoTable] t1
  INNER LOOP JOIN [DemoTable] t2 ON t1.ID = t2.ID;

Étant donné que j'exécutais ces requêtes sur SQL Server 2019 CU1, les plans d'exécution incluaient les informations de statistiques d'attente réelles liées à l'exécution de la requête.

Remarque : L'optimiseur utiliserait une jointure par fusion sans que les conseils soient appliqués pour cet ensemble de données et cette requête spécifiques.

Résultats des tests initiaux

Pour les tests initiaux, j'ai simplement utilisé SSMS pour exécuter les requêtes et collecté le plan d'exécution réelle pour comparer les informations d'attente associées à chaque requête qui sont présentées ci-dessous. Notez que pour cette taille de données, les temps écoulés ne sont pas significativement différents, pas plus que les temps d'attente ou le nombre d'attentes pour ASYNC_NETWORK_IO.

HASH JOIN

<WaitStats>
  <Wait WaitType="CXPACKET"         WaitTimeMs="18393" WaitCount="8415" />
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="4394"  WaitCount="6635" />
  <Wait WaitType="HTDELETE"         WaitTimeMs="957"   WaitCount="6"    />
  <Wait WaitType="HTBUILD"          WaitTimeMs="4"     WaitCount="6"    />
  <Wait WaitType="HTREPARTITION"    WaitTimeMs="3"     WaitCount="6"    />
  <Wait WaitType="CMEMTHREAD"       WaitTimeMs="3"     WaitCount="14"   />
  <Wait WaitType="LATCH_EX"         WaitTimeMs="2"     WaitCount="8"    />
</WaitStats>
<QueryTimeStats CpuTime="1068" ElapsedTime="4961" />

FUSIONNER JOINDRE

<WaitStats>
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="3169" WaitCount="6592" />
</WaitStats>
<QueryTimeStats CpuTime="792" ElapsedTime="3933" />

JOINTURE EN BOUCLE

<WaitStats>
  <Wait WaitType="CXPACKET"         WaitTimeMs="13690" WaitCount="8286" />
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="3576"  WaitCount="6631" />
  <Wait WaitType="LATCH_EX"         WaitTimeMs="1"     WaitCount="3"    />
</WaitStats>
<QueryTimeStats CpuTime="2172" ElapsedTime="4084" />

Cependant, ce n'était pas là que je voulais arrêter les tests, car ma propre expérience a montré à plusieurs reprises que Management Studio est un consommateur très inefficace des résultats de SQL Server et peut lui-même causer ASYNC_NETWORK_IO attend de se produire. J'ai donc décidé de changer la façon dont je testais les choses et je suis passé à une exécution SQLCMD des requêtes.

Test avec SQLCMD

Comme j'utilise beaucoup SQLCMD pour les démos lors de mes présentations, j'ai créé un fichier testscript.sql avec le contenu suivant :

PRINT 'Minimize Screen';
GO
 
WAITFOR DELAY '00:00:05';
GO
 
SELECT t1.ID, t2.FILLER, t2.FILLER
  FROM [DemoTable] t1
  INNER HASH JOIN [DemoTable] t2 ON t1.ID = t2.ID;
GO
 
SELECT t1.ID, t2.FILLER, t2.FILLER
  FROM [DemoTable] t1
  INNER MERGE JOIN [DemoTable] t2 ON t1.ID = t2.ID;
GO
 
SELECT t1.ID, t2.FILLER, t2.FILLER
  FROM [DemoTable] t1
  INNER LOOP JOIN [DemoTable] t2 ON t1.ID = t2.ID;
GO

Cela a été exécuté à partir de la ligne de commande comme suit, et pendant le délai de 5 secondes, la fenêtre a été minimisée pour permettre à l'exécution de ne pas afficher et faire défiler les résultats pendant le traitement :

sqlcmd -S.\SQL2019 -i testscript.sql -dAdventureWorks2017

Pour capturer les plans d'exécution réels, j'ai opté pour une session d'événements étendus collectant l'événement query_post_execution_showplan qui, avec le recul, sur SQL Server 2019, je pensais que j'aurais dû utiliser query_post_execution_plan_profile à la place pour utiliser l'implémentation légère de l'infrastructure de profilage des statistiques d'exécution des requêtes v3, mais cet événement ne renvoie pas les informations WaitStats ou QueryTimeStats sauf si query_post_execution_showplan est également activé en même temps. De plus, comme il s'agit d'une machine de test isolée sans autre charge de travail, les impacts du profilage standard ne sont pas vraiment un gros problème ici.

CREATE EVENT SESSION [Actual Plan] ON SERVER 
  ADD EVENT sqlserver.query_post_execution_showplan
  (ACTION(sqlserver.session_id));

HASH JOIN

<WaitStats>
  <Wait WaitType="CXPACKET"         WaitTimeMs="45722" WaitCount="8674" />
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="11321" WaitCount="6610" />
  <Wait WaitType="HTDELETE"         WaitTimeMs="1174"  WaitCount="6"    />
  <Wait WaitType="HTREPARTITION"    WaitTimeMs="4"     WaitCount="6"    />
  <Wait WaitType="HTBUILD"          WaitTimeMs="3"     WaitCount="5"    />
  <Wait WaitType="LATCH_EX"         WaitTimeMs="2"     WaitCount="7"    />
</WaitStats>
<QueryTimeStats ElapsedTime="11874" CpuTime="1070" />

FUSIONNER JOINDRE

<WaitStats>
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="10837" WaitCount="6602" />
</WaitStats>
<QueryTimeStats ElapsedTime="11597" CpuTime="789" />

JOINTURE EN BOUCLE

<WaitStats>
  <Wait WaitType="CXPACKET"         WaitTimeMs="43587" WaitCount="8620" />
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="11177" WaitCount="6612" />
  <Wait WaitType="LATCH_EX"         WaitTimeMs="1"     WaitCount="3"    />
</WaitStats>
<QueryTimeStats ElapsedTime="11696" CpuTime="2221" />

Cela n'a en fait pas fonctionné comme un moyen plus rapide d'exécuter la requête, et les performances ont en fait été réduites en utilisant l'utilitaire de ligne de commande pour exécuter la requête, même lorsque la fenêtre est réduite et ne fait pas défiler visiblement les résultats. Avec la fenêtre ouverte, le temps d'exécution HASH était de 15708 ms et le temps d'attente ASYNC_NETWORK_IO était de 15126 ms. Cependant, cela démontre que pour les mêmes résultats exacts, les performances du client consommant les résultats impactent à la fois le temps d'attente et le temps d'exécution de la requête.

Impact du parallélisme ?

L'une des choses que j'ai remarquées est que seuls deux des plans s'étaient exécutés avec parallélisme, sur la base de l'existence des attentes CXPACKET et LATCH_EX dans le plan d'exécution XML. Je me suis donc demandé quel impact le fait de forcer un plan d'exécution en série aurait sur l'exécution de ces mêmes requêtes en utilisant OPTION (MAXDOP 1).

HASH JOIN

<WaitStats>
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="4047" WaitCount="6379" />
</WaitStats>
<QueryTimeStats CpuTime="602" ElapsedTime="4619" />

FUSIONNER JOINDRE

<WaitStats>
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="3699" WaitCount="6608" />
</WaitStats>
<QueryTimeStats CpuTime="810" ElapsedTime="4478" />

JOINTURE EN BOUCLE

<WaitStats>
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="2083" WaitCount="5385" />
</WaitStats>
<QueryTimeStats CpuTime="1859" ElapsedTime="3918" />

Notez ici que le nombre total d'attentes n'a pas diminué de manière significative. Seul le plan de jointure de boucle série a un changement majeur dans le nombre d'attentes ou le temps d'attente total qui lui est associé, et isolément, cela ne signifie pas que c'est un avantage positif, le temps d'exécution de la requête n'a pas été significativement amélioré et il peut y avoir d'autres facteurs qui ont affecté les résultats de ce test spécifique.

Le tableau ci-dessous résume le temps d'attente et le décompte ASYNC_NETWORK_IO pour chacun des tests.

PlanType Lignes WaitCount Temps d'attente ExecTime Nom de l'application MAXDOP 1 Parallèle
Hachage 250 000 6 635 4 394 4 961 SSMS N Oui
Fusionner 250 000 6 592 3 169 3 933 SSMS N N
Boucle 250 000 6 631 3 576 4 084 SSMS N Oui
Hachage 250 000 6 610 11 321 11 874 SQLCMD N Oui
Fusionner 250 000 6 602 10 837 11 597 SQLCMD N N
Boucle 250 000 6 612 11 177 11 696 SQLCMD N Oui
Hachage 250 000 6 379 4 047 4 619 SSMS Oui N
Fusionner 250 000 6 608 3 699 4 479 SSMS Oui N
Boucle 250 000 5 385 2 083 3 918 SSMS Oui N

Résumé

Bien que l'enquête de cet article ne couvre pas tous les aspects des modifications de plan ou du type d'attente ASYNC_NETWORK_IO, elle démontre que cette attente n'est pas affectée de manière significative par le plan d'exécution utilisé pour l'exécution d'une requête. Je classerais ce type d'attente presque comme le type d'attente CXPACKET lors de l'analyse d'un serveur dans son ensemble; normal à voir pour la plupart des charges de travail et à moins qu'il ne soit incroyablement biaisé et qu'il existe d'autres problèmes de performances indiquant une consommation lente des résultats par les clients, comme le blocage avec le bloqueur de plomb en attente d'ASYNC_NETWORK_IO, alors quelque chose à ignorer car juste "une partie de la signature d'attente normale pour la charge de travail".