Pläne mit XML-Readern optimieren

34

Ausführen der Abfrage von hier aus , um die Deadlock-Ereignisse aus der erweiterten Standardereignissitzung zu entfernen

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';

Die Ausführung auf meinem Computer dauert ungefähr 20 Minuten. Die gemeldeten Statistiken sind

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.

Langsames Planen von XML

Parallel

Wenn ich die WHEREKlausel entferne , wird sie in weniger als einer Sekunde abgeschlossen und liefert 3.782 Zeilen.

Ebenso, wenn ich OPTION (MAXDOP 1)zu der ursprünglichen Abfrage hinzufüge , die die Dinge auch mit den Statistiken beschleunigt, die jetzt massiv weniger Lob-Lesevorgänge anzeigen.

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.

Schnellerer XML-Plan

Seriell

Also meine Frage ist

Kann jemand erklären, was los ist? Warum ist der ursprüngliche Plan so katastrophal und gibt es eine verlässliche Möglichkeit, das Problem zu umgehen?

Zusatz:

Ich habe auch festgestellt, dass das Ändern der Abfrage INNER HASH JOINdie Dinge in gewissem Maße verbessert (aber es dauert immer noch> 3 Minuten), da die DMV-Ergebnisse so klein sind, dass ich bezweifle, dass der Join-Typ selbst dafür verantwortlich ist und davon ausgeht, dass sich etwas anderes geändert hat. Statistiken dafür

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.

(Und plan)

Nach dem Auffüllen des erweiterten Ereignisringpuffers ( DATALENGTHder XML4.880.045 Byte enthielt und 1.448 Ereignisse enthielt) und dem Testen einer gekürzten Version der ursprünglichen Abfrage mit und ohne MAXDOPHinweis.

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 

Ergab die folgenden Ergebnisse

+-------------------------------------+------+----------+
|                                     | 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 |
+-------------------------------------+------+----------+

Es gibt einen deutlichen Unterschied in der Zuweisung von Tempdb zu der schnelleren, die anzeigt, dass 616Seiten zugewiesen und freigegeben wurden. Dies ist die gleiche Anzahl von Seiten, die verwendet werden, wenn das XML auch in eine Variable eingefügt wird.

Für den langsamen Plan liegen diese Seitenzuordnungszahlen in Millionenhöhe. Die Abfrage dm_db_task_space_usagewährend der Ausführung der Abfrage zeigt, dass ständig tempdbSeiten zugewiesen und freigegeben werden, wobei zwischen 1.800 und 3.000 Seiten gleichzeitig zugewiesen werden.

Martin Smith
quelle
Sie können die WHEREKlausel in den XQuery-Ausdruck verschieben. die Logik muss nicht entfernt werden , damit es schnell gehen: TargetData.nodes ('RingBufferTarget[1]/event[@name = "xml_deadlock_report"]'). Trotzdem kenne ich XML-Interna nicht gut genug, um die von Ihnen gestellte Frage zu beantworten.
Jon Seigel
Paging @SQLPoolBoy für Sie Martin ... er schlug vor, die Kommentare hier durchzugehen , wo er effizientere Vorschläge hat (sie basieren auf dem Quellartikel für den obigen Code ).
Aaron Bertrand

Antworten:

36

Der Grund für den Leistungsunterschied liegt darin, wie skalare Ausdrücke in der Ausführungsengine behandelt werden. In diesem Fall ist der Ausdruck des Interesses:

[Expr1000] = CONVERT(xml,DM_XE_SESSION_TARGETS.[target_data],0)

Dieser Ausdruck Etikett wird definiert durch einen Compute Scalar - Operator (Knoten 11 in dem seriellen Plan, den Knoten 13 in dem parallelen Plan). Compute Scalar-Operatoren unterscheiden sich von anderen Operatoren (ab SQL Server 2005) darin, dass die von ihnen definierten Ausdrücke nicht unbedingt an der Position ausgewertet werden, an der sie im sichtbaren Ausführungsplan erscheinen. Die Auswertung kann verschoben werden, bis das Ergebnis der Berechnung von einem späteren Bediener benötigt wird.

In der vorliegenden Abfrage ist die target_dataZeichenfolge normalerweise groß, wodurch die Konvertierung von Zeichenfolge zu XMLteuer wird. In langsamen Plänen wird die zu XMLkonvertierende Zeichenfolge jedes Mal ausgeführt, wenn ein späterer Operator, der das Ergebnis von benötigt, erneut Expr1000gebunden wird.

Das erneute Binden erfolgt an der Innenseite eines Joins mit verschachtelten Schleifen, wenn sich ein korrelierter Parameter (äußere Referenz) ändert. Expr1000ist eine äußere Referenz für die meisten der in diesem Ausführungsplan verbundenen verschachtelten Schleifen. Der Ausdruck wird von mehreren XML-Readern, sowohl von Stream-Aggregaten als auch von einem Startfilter, mehrfach referenziert. Abhängig von der Größe von XMLkann die Anzahl der Konvertierungen der Zeichenfolge XMLleicht in den Millionenbereich fallen.

Die folgenden Aufruflisten zeigen Beispiele für die target_dataKonvertierung der Zeichenfolge in XML( ConvertStringToXMLForES- wobei ES der Ausdrucksservice ist ):

Startfilter

Start Filter Aufrufliste

XML Reader (TVF Stream intern)

TVF Stream-Aufrufliste

Aggregat streamen

Stream Aggregate-Aufrufliste

Das Konvertieren der Zeichenfolge bei XMLjeder erneuten Bindung eines dieser Operatoren erklärt den Leistungsunterschied, der bei den Plänen für verschachtelte Schleifen beobachtet wird. Dies ist unabhängig davon, ob Parallelität verwendet wird oder nicht. Es kommt einfach so vor, dass der Optimierer einen Hash-Join auswählt, wenn der MAXDOP 1Hinweis angegeben wird. Wenn MAXDOP 1, LOOP JOINangegeben, ist die Leistung genauso schlecht wie beim parallelen Standardplan (bei dem der Optimierer verschachtelte Schleifen auswählt).

Wie viel Leistung mit einem Hash-Join zunimmt, hängt davon ab, ob Expr1000auf der Build- oder der Testseite des Operators angezeigt wird. Die folgende Abfrage sucht den Ausdruck auf der Testseite:

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';

Ich habe die schriftliche Reihenfolge der Joins gegenüber der in der Frage gezeigten Version umgekehrt, da Join-Hinweise ( INNER HASH JOINoben) auch die Reihenfolge für die gesamte Abfrage erzwingen, so als ob FORCE ORDERsie angegeben worden wären . Die Umkehrung ist notwendig, um sicherzustellen Expr1000, dass auf der Sondenseite erscheint. Der interessante Teil des Ausführungsplans ist:

Hinweis 1

Mit dem auf der Testseite definierten Ausdruck wird der Wert zwischengespeichert:

Hash-Cache

Die Auswertung von Expr1000wird immer noch zurückgestellt, bis der erste Operator den Wert benötigt (der Startfilter im Stack-Trace oben), der berechnete Wert jedoch zwischengespeichert ( CValHashCachedSwitch) und für spätere Aufrufe durch die XML-Reader und Stream-Aggregate wiederverwendet wird. Die folgende Stapelverfolgung zeigt ein Beispiel für den zwischengespeicherten Wert, der von einem XML-Reader wiederverwendet wird.

Cache-Wiederverwendung

Wenn die Join-Reihenfolge so erzwungen wird, dass die Definition von Expr1000auf der Build-Seite des Hash-Joins erfolgt, ist die Situation anders:

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

Ein Hash-Join liest seine Build-Eingabe vollständig, um eine Hash-Tabelle zu erstellen, bevor er nach Übereinstimmungen sucht. Infolgedessen müssen wir alle Werte speichern , nicht nur den pro Thread, an dem auf der Testseite des Plans gearbeitet wird. Die Hashverknüpfung verwendet daher eine tempdbArbeitstabelle zum Speichern der XMLDaten, und jeder Zugriff auf das Ergebnis Expr1000durch spätere Operatoren erfordert eine kostspielige Reise nach tempdb:

Langsamer Zugriff

Im Folgenden finden Sie weitere Details zum langsamen Zugriffspfad:

Langsame Details

Wenn ein Merge-Join erzwungen wird, werden die Eingabezeilen sortiert (eine Blockierungsoperation, genau wie die Build-Eingabe für einen Hash-Join). Dies führt zu einer ähnlichen Anordnung, bei der tempdbaufgrund der Datengröße ein langsamer Zugriff über eine sortierungsoptimierte Arbeitstabelle erforderlich ist.

Pläne, die große Datenmengen bearbeiten, können aus allen möglichen Gründen problematisch sein, die aus dem Ausführungsplan nicht hervorgehen. Die Verwendung eines Hash-Joins (mit dem Ausdruck für die richtige Eingabe) ist keine gute Lösung. Es basiert auf undokumentiertem internen Verhalten, ohne Garantie, dass es nächste Woche auf die gleiche Weise funktioniert, oder auf einer geringfügig anderen Abfrage.

Die Botschaft ist, dass XMLManipulationen heute schwierig zu optimieren sein können. Das Schreiben XMLin eine variable oder temporäre Tabelle vor dem Zerkleinern ist eine viel zuverlässigere Lösung als alles, was oben gezeigt wurde. Ein Weg dies zu tun ist:

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';

Zum Schluss möchte ich noch Martins sehr schöne Grafik aus den Kommentaren unten hinzufügen:

Martins Grafik

Paul White sagt GoFundMonica
quelle
Tolle Erklärung, danke. Ich hatte Ihren Artikel über Rechenskalare ebenfalls gelesen, aber hier nicht zwei und zwei zusammengestellt.
Martin Smith
3
Ich muss gestern etwas mit meinem Versuch, ein Profil zu erstellen, durcheinander gebracht haben (vielleicht langsame und schnelle Spuren durcheinander gebracht!). Ich habe es heute überarbeitet und natürlich zeigt es nur, was Sie bereits gesagt haben.
Martin Smith
2
Ja, der Screenshot ist der Call Tree View-Bericht aus dem Visual Studio 2012-Profiler . Ich denke, die Methodennamen sehen in Ihrer Ausgabe viel klarer aus, ohne dass mysteriöse Zeichenfolgen wie zum Beispiel @@IEAAXPEA_Kangezeigt werden.
Martin Smith
10

Das ist der Code aus meinem Artikel, der ursprünglich hier gepostet wurde:

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

Wenn Sie die Kommentare lesen, finden Sie eine Reihe von Alternativen, bei denen die auftretenden Leistungsprobleme nicht auftreten. Eine verwendet eine Modifikation dieser ursprünglichen Abfrage und die andere eine Variable, um das XML zu speichern, bevor es verarbeitet wird besser. (siehe meine Kommentare auf Seite 2) XML von den DMVs kann langsam verarbeitet werden, ebenso wie das Parsen von XML von der DMF für das Dateiziel, was häufig besser erreicht wird, indem die Daten zuerst in eine temporäre Tabelle eingelesen und dann verarbeitet werden. XML in SQL ist im Vergleich zur Verwendung von .NET oder SQLCLR langsam.

Jonathan Kehayias
quelle
1
Vielen Dank! Das hat den Trick gemacht. Derjenige ohne die Variable 600ms und 6341 liest und mit der Variablen 303 msund 3249 lob reads. 2012 musste ich auch and target_name='ring_buffer'diese Version ergänzen , da es jetzt zwei Ziele zu geben scheint. Ich versuche immer noch, mir ein Bild davon zu machen, was genau es in der 20-minütigen Version macht.
Martin Smith