Ottimizzare i piani con lettori XML


34

Eseguire la query da qui per estrarre gli eventi deadlock dalla sessione predefinita degli eventi estesi

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

il completamento della mia macchina richiede circa 20 minuti. Le statistiche riportate sono

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 piano lento

Parallelo

Se rimuovo la WHEREclausola si completa in meno di un secondo restituendo 3.782 righe.

Allo stesso modo se aggiungo OPTION (MAXDOP 1)alla query originale che accelera anche le cose con le statistiche che ora mostrano un numero enorme di letture lob.

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.

XML piano più veloce

Seriale

Quindi la mia domanda è

Qualcuno può spiegare cosa sta succedendo? Perché il piano originale è così catastroficamente peggiore e esiste un modo affidabile per evitare il problema?

aggiunta:

Ho anche scoperto che cambiare la query per INNER HASH JOINmigliorare le cose in una certa misura (ma ci vogliono ancora> 3 minuti) poiché i risultati DMV sono così piccoli che dubito che il tipo Join stesso sia responsabile e presumo che qualcos'altro debba essere cambiato. Statistiche per quello

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.

(E piano)

Dopo aver riempito il buffer eventi estesi anello ( DATALENGTHdella XMLera 4,880,045 byte e conteneva 1.448 eventi.) E test di un taglio giù versione della query originale con e senza il MAXDOPsuggerimento.

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 

Ha dato i seguenti risultati

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

C'è una chiara differenza nelle allocazioni tempdb con quella più veloce che mostra le 616pagine allocate e deallocate. Questa è la stessa quantità di pagine utilizzate quando anche l'XML viene inserito in una variabile.

Per il piano lento, questi conteggi di allocazione delle pagine sono in milioni. Il polling dm_db_task_space_usagementre la query è in esecuzione mostra che sembra che stia allocando e deallocando costantemente le pagine in tempdbqualsiasi momento tra 1.800 e 3.000 pagine allocate contemporaneamente.


È possibile spostare la WHEREclausola nell'espressione XQuery; la logica non deve essere rimosso per poter andare veloce: TargetData.nodes ('RingBufferTarget[1]/event[@name = "xml_deadlock_report"]'). Detto questo, non conosco abbastanza bene gli interni XML per rispondere alla domanda che hai posto.
Jon Seigel,

Paging @SQLPoolBoy per te Martin ... ti ha suggerito di leggere i commenti qui dove ha suggerimenti più efficienti (sono basati sull'articolo di origine per il codice sopra ).
Aaron Bertrand

Risposte:


36

Il motivo della differenza di prestazioni risiede nel modo in cui le espressioni scalari vengono gestite nel motore di esecuzione. In questo caso, l'espressione di interesse è:

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

Questa etichetta di espressione è definita da un operatore di calcolo scalare (nodo 11 nel piano seriale, nodo 13 nel piano parallelo). Gli operatori scalari di calcolo sono diversi dagli altri operatori (da SQL Server 2005 in poi) in quanto le espressioni che definiscono non sono necessariamente valutate nella posizione in cui compaiono nel piano di esecuzione visibile; la valutazione può essere rinviata fino a quando il risultato del calcolo non sarà richiesto da un operatore successivo.

Nella presente query, la target_datastringa è in genere grande, rendendo la conversione da stringa a XMLcostosa. Nei piani lenti, la stringa da XMLconvertire viene eseguita ogni volta che Expr1000viene rimbalzato un operatore successivo che richiede il risultato .

La rilegatura si verifica sul lato interno di un'unione di cicli nidificati quando cambia un parametro correlato (riferimento esterno). Expr1000è un riferimento esterno per la maggior parte dei join di loop nidificati in questo piano di esecuzione. L'espressione viene citata più volte da diversi lettori XML, sia Stream Aggregate, sia da un filtro di avvio. A seconda della dimensione di XML, il numero di volte in cui viene convertita la stringa XMLpuò facilmente essere numerato in milioni.

Le pile di chiamate seguenti mostrano esempi della target_datastringa convertita in XML( ConvertStringToXMLForES- dove ES è il servizio di espressione ):

Filtro di avvio

Start-up Filtro stack chiamate

Lettore XML (flusso TVF internamente)

Stack di chiamate Stream TVF

Stream Aggregate

Stack di chiamate aggregate Stream

La conversione della stringa XMLogni volta che uno di questi operatori si ricollega spiega la differenza di prestazioni osservata con i piani di cicli nidificati. Questo indipendentemente dal fatto che il parallelismo sia usato o meno. Accade solo che l'ottimizzatore scelga un hash join quando MAXDOP 1viene specificato il suggerimento. Se MAXDOP 1, LOOP JOINspecificato, le prestazioni sono scarse come nel piano parallelo predefinito (in cui l'ottimizzatore sceglie i cicli nidificati).

L'aumento delle prestazioni con un hash join dipende dal fatto che Expr1000appaia sul lato build o probe dell'operatore. La query seguente individua l'espressione sul lato sonda:

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

Ho invertito l'ordine scritto dei join dalla versione mostrata nella domanda, perché i suggerimenti sui join ( INNER HASH JOINsopra) forzano anche l'ordine per l'intera query, proprio come se FORCE ORDERfosse stato specificato. L'inversione è necessaria per assicurarsi che Expr1000appaia sul lato della sonda. La parte interessante del piano di esecuzione è:

suggerimento 1

Con l'espressione definita sul lato della sonda, il valore viene memorizzato nella cache:

Hash Cache

La valutazione di Expr1000è ancora rinviata fino a quando il primo operatore ha bisogno del valore (il filtro di avvio nella traccia dello stack sopra) ma il valore calcolato viene memorizzato nella cache ( CValHashCachedSwitch) e riutilizzato per le chiamate successive da parte dei lettori XML e degli aggregati di flusso. La traccia dello stack di seguito mostra un esempio del valore memorizzato nella cache che viene riutilizzato da un lettore XML.

Riutilizzo della cache

Quando l'ordine di join viene forzato in modo tale che la definizione di si Expr1000verifica sul lato build dell'hash join, la situazione è diversa:

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

Un join hash legge completamente l'input di compilazione per costruire una tabella hash prima che inizi a sondare le corrispondenze. Di conseguenza, dobbiamo memorizzare tutti i valori, non solo quello per thread su cui si lavora dal lato della sonda del piano. L'hash join pertanto utilizza una tempdbtabella di lavoro per archiviare i XMLdati e ogni accesso al risultato di Expr1000operatori successivi richiede un viaggio costoso per tempdb:

Accesso lento

Quanto segue mostra ulteriori dettagli sul percorso di accesso lento:

Dettagli lenti

Se un join di unione viene forzato, le righe di input vengono ordinate (un'operazione di blocco, proprio come l'input di compilazione di un join di hash), risultando in una disposizione simile in cui tempdbè richiesto un accesso lento tramite un worktable ottimizzato per l'ordinamento a causa della dimensione dei dati.

I piani che manipolano elementi di dati di grandi dimensioni possono essere problematici per tutti i tipi di ragioni che non sono evidenti dal piano di esecuzione. L'uso di un hash join (con l'espressione sull'input corretto) non è una buona soluzione. Si basa su un comportamento interno non documentato senza garanzie che funzionerà allo stesso modo la settimana prossima o su una query leggermente diversa.

Il messaggio è che la XMLmanipolazione può essere una cosa difficile da ottimizzare oggi. Scrivere XMLin una tabella variabile o temporanea prima della distruzione è una soluzione molto più solida di qualsiasi altra cosa mostrata sopra. Un modo per farlo è:

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

Infine, voglio solo aggiungere la grafica molto bella di Martin dai commenti qui sotto:

Grafica di Martin


Grande spiegazione, grazie. Avevo letto anche il tuo articolo sugli scalari di calcolo ma non ho messo insieme due e due qui.
Martin Smith,

3
Devo aver incasinato qualcosa con il mio tentativo di profilare ieri (forse confondendo tracce lente e veloci!). L'ho rifatto oggi e ovviamente mostra solo quello che hai già detto.
Martin Smith,

2
Sì, lo screenshot è il rapporto Visualizzazione struttura ad albero del profiler di Visual Studio 2012 . Penso che i nomi dei metodi appaiano molto più chiari nel tuo output anche se senza stringhe misteriose come @@IEAAXPEA_Kapparire.
Martin Smith,

10

Questo è il codice del mio articolo originariamente pubblicato qui:

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

Se leggi i commenti troverai un paio di alternative che non presentano i problemi di prestazioni che stai riscontrando, uno che utilizza una modifica della query originale e l'altro che utilizza una variabile per contenere l'XML prima di elaborarlo che funziona meglio. (vedere i miei commenti su Pagina 2) L'XML dal DMV può essere lento nell'elaborare, così come l'analisi dell'XML dal DMF per la destinazione del file che spesso si ottiene meglio leggendo prima i dati in una tabella temporanea e quindi elaborandola. XML in SQL è lento rispetto all'utilizzo di cose come .NET o SQLCLR.


1
Grazie! Questo ha funzionato. Quello senza la variabile che prende 600ms e 6341 legge e con la variabile 303 mse 3249 lob reads. Nel 2012 dovevo aggiungere anche and target_name='ring_buffer'a quella versione in quanto sembra che ora ci siano due obiettivi. Sto ancora cercando di ottenere un'immagine mentale di quello che sta facendo esattamente nella versione di 20 minuti.
Martin Smith,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.