Optimiser les plans avec des lecteurs XML

34

Exécution de la requête à partir d'ici pour extraire les événements d'interblocage 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 à compléter 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.

Plan lent XML

Parallèle

Si je supprime la WHEREclause, elle se termine en moins d’une seconde et renvoie 3 782 lignes.

De la même manière, si j'ajoute OPTION (MAXDOP 1)à la requête initiale une accélération du processus, les statistiques affichent désormais un nombre de lectures massivement moins élevé.

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 plus rapide XML

En série

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?

Une addition:

J'ai également constaté que modifier la requête INNER HASH JOINaméliore quelque peu les choses (mais cela prend toujours plus de 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 que quelque chose d'autre 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 DATALENGTHd’ XMLannonce des événements étendus (4 880 045 octets, contenant 1448 événements), et testé une version réduite de la requête initiale avec et sans MAXDOPindication.

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 existe une nette différence dans les allocations de tempdb, la plus rapide montrant que les 616pages ont été allouées et désallouées. Cela correspond au même nombre de pages utilisées lorsque le XML est également placé dans une variable.

Pour le plan lent, ces comptes d'allocation de pages se chiffrent en millions. Le fait de scruter dm_db_task_space_usagependant que la requête est en cours montre qu’il semble allouer et désallouer en permanence des pages, tempdbavec entre 1 800 et 3 000 pages allouées à la fois.

Martin Smith
la source
Vous pouvez déplacer la WHEREclause dans l'expression XQuery. la logique ne doit pas être supprimé car pour aller vite: TargetData.nodes ('RingBufferTarget[1]/event[@name = "xml_deadlock_report"]'). Cela dit, je ne connais pas suffisamment bien les composants internes XML pour répondre à la question que vous avez posée.
Jon Seigel
Paging @SQLPoolBoy pour vous Martin ... il suggère de passer en revue les commentaires où il a des suggestions plus efficaces (elles sont basées sur l’ article source du code ci-dessus ).
Aaron Bertrand

Réponses:

36

La différence de performances tient à la manière 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 Compute Scalar (noeud 11 dans le plan série, noeud 13 dans le plan parallèle). Les opérateurs de calcul Scalar sont différents des autres opérateurs (SQL Server 2005 et ultérieur) en ce que les expressions qu'ils définissent ne sont pas nécessairement évalués à 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 requête actuelle, la target_datachaîne est généralement longue, ce qui rend la conversion de chaîne en XMLchère. Dans les plans lents, la XMLconversion en chaîne est effectuée chaque fois qu'un opérateur ultérieur nécessitant le résultat de Expr1000son rebond.

La reliure a lieu du côté intérieur des boucles imbriquées qui se rejoignent lorsqu'un paramètre corrélé (référence externe) change. Expr1000est 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 Agrégats de flux et par un filtre de démarrage. En fonction de la taille de la XML, le nombre de fois que la chaîne est convertie enXML peut facilement se chiffrer en millions.

Les piles d’appel ci-dessous montrent des exemples de target_datachaîne convertie en XML( ConvertStringToXMLForES- où ES est le service d’expression ):

Filtre de démarrage

Filtre d'appels de démarrage

Lecteur XML (flux TVF en interne)

Pile d'appels de flux TVF

Agrégat de flux

Pile d'appels en flux agrégé

La conversion de la chaîne à XMLchaque fois que l'un de ces opérateurs effectue un rapprochement explique la différence de performances observée avec les plans de boucles imbriquées. Ceci indépendamment du fait que le parallélisme soit utilisé ou non. Il se trouve que l'optimiseur choisit une jointure de hachage lorsque l' MAXDOP 1indicateur est spécifié. SiMAXDOP 1, LOOP JOIN est spécifié, les performances sont médiocres, tout comme avec le plan parallèle par défaut (où l'optimiseur choisit des boucles imbriquées).

L'augmentation des performances avec une jointure de hachage dépend Expr1000de l'affichage ou non de la construction ou de la sonde de l'opérateur. La requête suivante localise l'expression du côté de la 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 par rapport à la version indiquée dans la question, car les indications de jointure ( INNER HASH JOINci-dessus) forcent également l'ordre de la requête entière, comme si cela FORCE ORDERavait été spécifié. L’inversion est nécessaire pour que l’ Expr1000apparence apparaisse du côté de la sonde. La partie intéressante du plan d'exécution est la suivante:

indice 1

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

Cache de hachage

L'évaluation de Expr1000est toujours différée 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 les 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 en cache réutilisée par un lecteur XML.

Réutilisation du cache

Lorsque l'ordre de jointure est forcé de telle sorte que la définition de Expr1000se produise 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 construction pour construire une table de hachage avant de commencer à rechercher des correspondances. En conséquence, nous devons stocker toutes les valeurs, pas uniquement celle par thread traitée du côté de la sonde du plan. La jointure de hachage utilise donc une tempdbtable de travail pour stocker les XMLdonnées, et chaque accès au résultat généré Expr1000par des opérateurs ultérieurs nécessite un trajet coûteux pour tempdb:

Accès lent

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

Détails lents

Si une jointure par fusion est forcée, les lignes d'entrée sont triées (une opération de blocage, tout comme l'entrée de construction dans une jointure de hachage), ce qui entraîne un arrangement similaire dans lequel un accès lent via une tempdbtable de travail à tri optimisé est requis en raison de la taille des données.

Les plans qui manipulent des éléments de données volumineux peuvent poser problème pour toutes sortes de raisons qui ne ressortent pas du 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 repose 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 XMLmanipulation peut être difficile à optimiser aujourd'hui. Écrire la XMLdans une table de variables ou temporaire avant le déchiquetage est une solution de contournement beaucoup plus solide que tout ce qui est présenté ci-dessus. Une façon de faire 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 tiré des commentaires ci-dessous:

Graphique de Martin

Paul White dit GoFundMonica
la source
Excellente explication, merci. J'avais aussi lu votre article sur les scalaires de calcul, mais ne mettez pas ici deux et deux ensemble.
Martin Smith
3
Je dois avoir foiré quelque chose avec ma tentative de profilage hier (peut-être confondu traces lentes et rapides!). Je l'ai refait aujourd'hui et, bien sûr, cela ne fait que montrer ce que vous avez déjà dit.
Martin Smith
2
Oui, la capture d'écran correspond au rapport Afficher l'arborescence des appels à partir du profileur Visual Studio 2012 . Je pense que les noms de méthodes ont une apparence beaucoup plus claire dans votre sortie, mais sans chaînes mystérieuses telles @@IEAAXPEA_Kqu'apparition.
Martin Smith
10

C'est le code de mon article initialement publié ici:

http://www.sqlservercentral.com/articles/deadlock/65658/

Si vous lisez les commentaires, vous trouverez plusieurs solutions qui ne présentent pas les problèmes de performances que vous rencontrez, l’une utilisant une modification de la requête initiale et l’autre utilisant une variable permettant de conserver le code XML avant de le traiter, ce qui fonctionne. mieux. (Voir mes commentaires à la page 2) Le traitement de XML à partir de fichiers DMV peut être lent, tout comme l'analyse de XML à partir de DMF pour le fichier cible, ce qui est souvent mieux accompli en lisant d'abord les données dans une table temporaire, puis en les traitant. XML en SQL est lent par rapport à l'utilisation de choses comme .NET ou SQLCLR.

Jonathan Kehayias
la source
1
Merci! Cela a fait le tour. Celui sans variable prenant 600ms et 6341 lit et avec la variable 303 mset 3249 lob reads. En 2012, je devais également ajouter and target_name='ring_buffer'à cette version, qui ressemble maintenant à deux cibles. J'essaie toujours d'obtenir une image mentale de ce que cela fait exactement dans la version 20 minutes.
Martin Smith