Exécution la requête d'ici pour extraire les événements de blocage de la session d'événements étendus par défaut
SELECT CAST (
REPLACE (
REPLACE (
XEventData.XEvent.value ('(data/value)[1]', 'varchar(max)'),
'<victim-list>', '<deadlock><victim-list>'),
'<process-list>', '</victim-list><process-list>')
AS XML) AS DeadlockGraph
FROM (SELECT CAST (target_data AS XML) AS TargetData
FROM sys.dm_xe_session_targets st
JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
WHERE [name] = 'system_health') AS Data
CROSS APPLY TargetData.nodes ('//RingBufferTarget/event') AS XEventData (XEvent)
WHERE XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report';
prend environ 20 minutes pour terminer sur ma machine. Les statistiques rapportées sont
Table 'Worktable'. Scan count 0, logical reads 68121, physical reads 0, read-ahead reads 0,
lob logical reads 25674576, lob physical reads 0, lob read-ahead reads 4332386.
SQL Server Execution Times:
CPU time = 1241269 ms, elapsed time = 1244082 ms.
Si je supprime la clause WHERE
, elle se termine en moins d'une seconde renvoyant 3 782 lignes.
De même, si j'ajoute OPTION (MAXDOP 1)
à la requête d'origine, cela accélère également les choses, les statistiques affichant désormais beaucoup moins de lectures de lobes.
Table 'Worktable'. Scan count 0, logical reads 15, physical reads 0, read-ahead reads 0,
lob logical reads 6767, lob physical reads 0, lob read-ahead reads 6076.
SQL Server Execution Times:
CPU time = 639 ms, elapsed time = 693 ms.
Quelqu'un peut-il expliquer ce qui se passe? Pourquoi le plan initial est-il si catastrophiquement pire et existe-t-il un moyen fiable d'éviter le problème?
Addition:
J'ai également constaté que changer la requête en INNER HASH JOIN
Améliore les choses dans une certaine mesure (mais cela prend encore> 3 minutes) car les résultats DMV sont si petits que je doute que le type de jointure lui-même soit responsable et présume quelque chose sinon, ça doit avoir changé. Statistiques pour ça
Table 'Worktable'. Scan count 0, logical reads 30294, physical reads 0, read-ahead reads 0,
lob logical reads 10741863, lob physical reads 0, lob read-ahead reads 4361042.
SQL Server Execution Times:
CPU time = 200914 ms, elapsed time = 203614 ms.
Après avoir rempli le tampon en anneau des événements étendus (DATALENGTH
du XML
était de 4 880 045 octets et contenait 1 448 événements.) Et testé une version réduite de la requête d'origine avec et sans le MAXDOP
indice.
SELECT COUNT(*)
FROM (SELECT CAST (target_data AS XML) AS TargetData
FROM sys.dm_xe_session_targets st
JOIN sys.dm_xe_sessions s
ON s.address = st.event_session_address
WHERE [name] = 'system_health') AS Data
CROSS APPLY TargetData.nodes ('//RingBufferTarget/event') AS XEventData (XEvent)
WHERE XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report'
SELECT*
FROM sys.dm_db_task_space_usage
WHERE session_id = @@SPID
A donné les résultats suivants
+-------------------------------------+------+----------+
| | Fast | Slow |
+-------------------------------------+------+----------+
| internal_objects_alloc_page_count | 616 | 1761272 |
| internal_objects_dealloc_page_count | 616 | 1761272 |
| elapsed time (ms) | 428 | 398481 |
| lob logical reads | 8390 | 12784196 |
+-------------------------------------+------+----------+
Il y a une nette différence dans les allocations tempdb, la plus rapide affichant 616
Pages allouées et désallouées. Il s'agit du même nombre de pages utilisées lorsque le XML est également placé dans une variable.
Pour le plan lent, le nombre d'allocations de pages se chiffre en millions. L'interrogation de dm_db_task_space_usage
Pendant l'exécution de la requête montre qu'il semble allouer et désallouer constamment des pages dans tempdb
avec entre 1 800 et 3 000 pages allouées à tout moment.
La raison de la différence de performances réside dans la façon dont les expressions scalaires sont gérées dans le moteur d'exécution. Dans ce cas, la manifestation d'intérêt est:
[Expr1000] = CONVERT(xml,DM_XE_SESSION_TARGETS.[target_data],0)
Cette étiquette d'expression est définie par un opérateur de calcul scalaire (noeud 11 dans le plan série, noeud 13 dans le plan parallèle). Les opérateurs de calcul scalaire sont différents des autres opérateurs (SQL Server 2005 et suivants) en ce que les expressions qu'ils définissent sont pas nécessairement évaluées à la position où ils apparaissent dans le plan d'exécution visible; l'évaluation peut être différée jusqu'à ce que le résultat du calcul soit requis par un opérateur ultérieur.
Dans la présente requête, le target_data
chaîne est généralement volumineuse, ce qui rend la conversion de chaîne en XML
coûteuse. Dans les plans lents, la conversion de chaîne en XML
est effectuée chaque fois qu'un opérateur ultérieur qui requiert le résultat de Expr1000
est un rebond.
La reliure se produit sur le côté intérieur d'une jointure de boucles imbriquées lorsqu'un paramètre corrélé (référence externe) change. Expr1000
est une référence externe pour la plupart des jointures de boucles imbriquées dans ce plan d'exécution. L'expression est référencée plusieurs fois par plusieurs lecteurs XML, à la fois des agrégats de flux et par un filtre de démarrage. Selon la taille de XML
, le nombre de fois que la chaîne est convertie en XML
peut facilement être numéroté en millions.
Les piles d'appels ci-dessous montrent des exemples de target_data
chaîne en cours de conversion en XML
(ConvertStringToXMLForES
- où ES est le Expression Service):
Filtre de démarrage
Lecteur XML (Stream TVF en interne)
Agrégat de flux
La conversion de la chaîne en XML
à chaque fois qu'un de ces opérateurs se lie à nouveau explique la différence de performances observée avec les plans de boucles imbriquées. Cela indépendamment du fait que le parallélisme soit utilisé ou non. Il se trouve que l'optimiseur choisit une jointure de hachage lorsque le MAXDOP 1
un indice est spécifié. Si MAXDOP 1, LOOP JOIN
est spécifié, les performances sont médiocres comme avec le plan parallèle par défaut (où l'optimiseur choisit des boucles imbriquées).
L'ampleur des performances avec une jointure par hachage dépend de si Expr1000
apparaît du côté de la construction ou de la sonde de l'opérateur. La requête suivante localise l'expression côté sonde:
SELECT CAST (
REPLACE (
REPLACE (
XEventData.XEvent.value ('(data/value)[1]', 'varchar(max)'),
'<victim-list>', '<deadlock><victim-list>'),
'<process-list>', '</victim-list><process-list>')
AS XML) AS DeadlockGraph
FROM (SELECT CAST (target_data AS XML) AS TargetData
FROM sys.dm_xe_sessions s
INNER HASH JOIN sys.dm_xe_session_targets st ON s.address = st.event_session_address
WHERE [name] = 'system_health') AS Data
CROSS APPLY TargetData.nodes ('//RingBufferTarget/event') AS XEventData (XEvent)
WHERE XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report';
J'ai inversé l'ordre écrit des jointures à partir de la version indiquée dans la question, car les indications de jointure (INNER HASH JOIN
ci-dessus) force également l'ordre pour la requête entière, comme si FORCE ORDER
avait été spécifié. L'inversion est nécessaire pour assurer Expr1000
apparaît côté sonde. La partie intéressante du plan d'exécution est:
Avec l'expression définie côté sonde, la valeur est mise en cache:
Évaluation de Expr1000
est toujours différé jusqu'à ce que le premier opérateur ait besoin de la valeur (le filtre de démarrage dans la trace de pile ci-dessus) mais la valeur calculée est mise en cache (CValHashCachedSwitch
) et réutilisée pour des appels ultérieurs par les lecteurs XML et les agrégats de flux . La trace de pile ci-dessous montre un exemple de la valeur mise en cache réutilisée par un lecteur XML.
Lorsque l'ordre de jointure est forcé de telle sorte que la définition de Expr1000
se produit du côté de la construction de la jointure de hachage, la situation est différente:
SELECT CAST (
REPLACE (
REPLACE (
XEventData.XEvent.value ('(data/value)[1]', 'varchar(max)'),
'<victim-list>', '<deadlock><victim-list>'),
'<process-list>', '</victim-list><process-list>')
AS XML) AS DeadlockGraph
FROM (SELECT CAST (target_data AS XML) AS TargetData
FROM sys.dm_xe_session_targets st
INNER HASH JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
WHERE [name] = 'system_health') AS Data
CROSS APPLY TargetData.nodes ('//RingBufferTarget/event') AS XEventData (XEvent)
WHERE XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report'
Une jointure de hachage lit complètement son entrée de génération pour construire une table de hachage avant de commencer à rechercher des correspondances. En conséquence, nous devons stocker tous les valeurs, pas seulement celle par thread sur laquelle on travaille depuis le côté sonde du plan. La jointure de hachage utilise donc une table de travail tempdb
pour stocker les données XML
et chaque accès au résultat de Expr1000
par les opérateurs ultérieurs nécessite un voyage coûteux vers tempdb
:
Ce qui suit montre plus de détails sur le chemin d'accès lent:
Si une jointure de fusion est forcée, les lignes d'entrée sont triées (une opération de blocage, tout comme l'entrée de génération d'une jointure de hachage), ce qui entraîne un arrangement similaire où un accès lent via une table de travail optimisée pour le tri tempdb
est requis en raison de la taille des données.
Les plans qui manipulent des éléments de données volumineux peuvent être problématiques pour toutes sortes de raisons qui n'apparaissent pas dans le plan d'exécution. L'utilisation d'une jointure de hachage (avec l'expression sur la bonne entrée) n'est pas une bonne solution. Il s'appuie sur un comportement interne non documenté sans aucune garantie qu'il fonctionnera de la même manière la semaine prochaine, ou sur une requête légèrement différente.
Le message est que la manipulation de XML
peut être difficile à optimiser aujourd'hui. L'écriture de XML
dans une table variable ou temporaire avant la destruction est une solution de contournement beaucoup plus solide que tout ce qui est illustré ci-dessus. Une façon de procéder est:
DECLARE @data xml =
CONVERT
(
xml,
(
SELECT TOP (1)
dxst.target_data
FROM sys.dm_xe_sessions AS dxs
JOIN sys.dm_xe_session_targets AS dxst ON
dxst.event_session_address = dxs.[address]
WHERE
dxs.name = N'system_health'
AND dxst.target_name = N'ring_buffer'
)
)
SELECT XEventData.XEvent.value('(data/value)[1]', 'varchar(max)')
FROM @data.nodes ('./RingBufferTarget/event[@name eq "xml_deadlock_report"]') AS XEventData (XEvent)
WHERE XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report';
Enfin, je veux juste ajouter le très joli graphique de Martin à partir des commentaires ci-dessous:
C'est le code de mon article initialement publié ici:
http://www.sqlservercentral.com/articles/deadlock/65658/
Si vous lisez les commentaires, vous trouverez quelques alternatives qui n'ont pas les problèmes de performances que vous rencontrez, l'une utilisant une modification de cette requête d'origine, et l'autre utilisant une variable pour conserver le XML avant de le traiter, ce qui fonctionne meilleur. (voir mes commentaires à la page 2) Le XML du DMV peut être lent à traiter, tout comme l'analyse XML du DMF pour la cible du fichier, ce qui est souvent mieux accompli en lisant d'abord les données dans une table temporaire puis en les traitant. XML dans SQL est lent par rapport à l'utilisation de choses comme .NET ou SQLCLR.