web-dev-qa-db-fra.com

Optimiser les plans avec des lecteurs XML

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.

XML à plan lent

Parallel

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.

Plan XML plus rapide

Serial

Donc ma question est

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.

(Et plan)

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.

35
Martin Smith

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

Start-up Filter call stack

Lecteur XML (Stream TVF en interne)

TVF Stream call stack

Agrégat de flux

Stream Aggregate call stack

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:

hint 1

Avec l'expression définie côté sonde, la valeur est mise en cache:

Hash 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.

Cache reuse

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'

Hash 2

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:

Slow access

Ce qui suit montre plus de détails sur le chemin d'accès lent:

Slow details

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:

Martin's graphic

37
Paul White 9

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.

10
Jonathan Kehayias