Optimizando planes con lectores XML

34

Ejecutar la consulta desde aquí para extraer los eventos de punto muerto de la sesión de eventos extendidos predeterminada

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

Tarda unos 20 minutos en completarse en mi máquina. Las estadísticas reportadas son

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 de plan lento

Paralela

Si elimino la WHEREcláusula, se completa en menos de un segundo y devuelve 3.782 filas.

Del mismo modo, si agrego OPTION (MAXDOP 1)a la consulta original, eso también acelera las cosas con las estadísticas que ahora muestran muchísimo menos lecturas de lóbulos.

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 más rápido

De serie

Entonces mi pregunta es

¿Alguien puede explicar lo que está pasando? ¿Por qué el plan original es tan catastróficamente peor y hay alguna forma confiable de evitar el problema?

Adición:

También descubrí que cambiar la consulta para INNER HASH JOINmejorar las cosas hasta cierto punto (pero aún toma más de 3 minutos) ya que los resultados del DMV son tan pequeños que dudo que el tipo de unión en sí sea responsable y supongo que algo más debe haber cambiado. Estadísticas para eso

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.

(Y plan)

Después de llenar el búfer de anillo de eventos extendidos ( DATALENGTHde XML4,880,045 bytes y contenía 1,448 eventos) y probar una versión reducida de la consulta original con y sin la MAXDOPsugerencia.

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 

Dio los siguientes resultados

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

Hay una clara diferencia en las asignaciones tempdb con la más rápida que muestra las 616páginas asignadas y desasignadas. Esta es la misma cantidad de páginas utilizadas cuando el XML también se coloca en una variable.

Para el plan lento, estos recuentos de asignación de páginas están en millones. El sondeo dm_db_task_space_usagemientras se ejecuta la consulta muestra que parece estar constantemente asignando y desasignando páginas tempdbcon entre 1.800 y 3.000 páginas asignadas en cualquier momento.

Martin Smith
fuente
Puede mover la WHEREcláusula a la expresión XQuery; la lógica no tiene que ser eliminado para que vaya rápido: TargetData.nodes ('RingBufferTarget[1]/event[@name = "xml_deadlock_report"]'). Dicho esto, no conozco los componentes internos de XML lo suficientemente bien como para responder la pregunta que ha planteado.
Jon Seigel
Paging @SQLPoolBoy para ti Martin ... sugirió revisar los comentarios aquí donde tiene sugerencias más eficientes (se basan en el artículo fuente del código anterior ).
Aaron Bertrand

Respuestas:

36

La razón de la diferencia de rendimiento radica en cómo se manejan las expresiones escalares en el motor de ejecución. En este caso, la expresión de interés es:

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

Esta etiqueta de expresión está definida por un operador Compute Scalar (nodo 11 en el plan en serie, nodo 13 en el plan paralelo). Los operadores de Compute Scalar son diferentes de otros operadores (SQL Server 2005 en adelante) en que las expresiones que definen no se evalúan necesariamente en la posición en que aparecen en el plan de ejecución visible; la evaluación puede diferirse hasta que un operador posterior requiera el resultado del cálculo.

En la presente consulta, la target_datacadena suele ser grande, lo que hace que la conversión de cadena a XMLcostosa. En planes lentos, la cadena de XMLconversión se realiza cada vez que un operador posterior que requiere el resultado del Expr1000rebote.

El rebobinado se produce en el lado interno de una unión de bucles anidados cuando cambia un parámetro correlacionado (referencia externa). Expr1000es una referencia externa para la mayoría de las uniones de bucles anidados en este plan de ejecución. Varios lectores XML hacen referencia a la expresión varias veces, tanto Stream Aggregates como un filtro de inicio. Dependiendo del tamaño de la XML, la cantidad de veces que se convierte la cadena XMLpuede ser fácilmente en millones.

Las pilas de llamadas a continuación muestran ejemplos de la target_datacadena que se está convirtiendo XML( ConvertStringToXMLForES- donde ES es el servicio de expresión ):

Filtro de arranque

Pila de llamadas de filtro de inicio

Lector XML (TVF Stream internamente)

Pila de llamadas de TVF Stream

Agregado de flujo

Stream Aggregate call stack

La conversión de la cadena a XMLcada uno de estos operadores vuelve a vincular explica la diferencia de rendimiento observada con los planes de bucles anidados. Esto es independientemente de si se usa el paralelismo o no. Sucede que el optimizador elige una unión hash cuando MAXDOP 1se especifica la sugerencia. Si MAXDOP 1, LOOP JOINse especifica, el rendimiento es deficiente al igual que con el plan paralelo predeterminado (donde el optimizador elige bucles anidados).

La cantidad de rendimiento que aumenta con una combinación hash depende de si Expr1000aparece en el lado de compilación o sonda del operador. La siguiente consulta localiza la expresión en el lado de la 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';

He invertido el orden escrito de las combinaciones de la versión que se muestra en la pregunta, porque las sugerencias de combinación ( INNER HASH JOINarriba) también fuerzan el orden de toda la consulta, como si se FORCE ORDERhubiera especificado. La inversión es necesaria para garantizar que Expr1000aparezca en el lado de la sonda. La parte interesante del plan de ejecución es:

pista 1

Con la expresión definida en el lado de la sonda, el valor se almacena en caché:

Hash Cache

La evaluación de Expr1000todavía se difiere hasta que el primer operador necesita el valor (el filtro de inicio en el seguimiento de la pila anterior) pero el valor calculado se almacena en caché ( CValHashCachedSwitch) y los lectores XML y agregados de flujo lo reutilizan para llamadas posteriores. El seguimiento de la pila a continuación muestra un ejemplo del valor almacenado en caché que un lector XML está reutilizando.

Reutilización de caché

Cuando el orden de unión se fuerza de tal manera que la definición de Expr1000ocurre en el lado de construcción de la unión hash, la situación es diferente:

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

Una combinación hash lee su entrada de compilación por completo para construir una tabla hash antes de que comience a buscar coincidencias. Como resultado, tenemos que almacenar todos los valores, no solo el de cada subproceso que se está trabajando desde el lado de la sonda del plan. Por lo tanto, la combinación hash utiliza una tempdbtabla de trabajo para almacenar los XMLdatos, y cada acceso al resultado de los Expr1000operadores posteriores requiere un viaje costoso para tempdb:

Acceso lento

A continuación se muestran más detalles de la ruta de acceso lento:

Detalles lentos

Si se fuerza una combinación de fusión, las filas de entrada se ordenan (una operación de bloqueo, al igual que la entrada de compilación para una combinación hash), lo que da como resultado una disposición similar en la que tempdbse requiere un acceso lento a través de una tabla de trabajo optimizada para la clasificación debido al tamaño de los datos.

Los planes que manipulan elementos de datos grandes pueden ser problemáticos por todo tipo de razones que no son evidentes en el plan de ejecución. Usar una combinación hash (con la expresión en la entrada correcta) no es una buena solución. Se basa en un comportamiento interno no documentado sin garantías de que funcionará de la misma manera la próxima semana, o en una consulta ligeramente diferente.

El mensaje es que la XMLmanipulación puede ser algo difícil de optimizar hoy. Escribir XMLen una tabla variable o temporal antes de la trituración es una solución mucho más sólida que cualquier cosa que se muestra arriba. Una forma de hacer esto es:

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

Finalmente, solo quiero agregar el bonito gráfico de Martin de los comentarios a continuación:

Gráfico de Martin

Paul White dice GoFundMonica
fuente
Gran explicación, gracias. También había leído su artículo sobre escalares computacionales, pero no puse dos y dos juntos aquí.
Martin Smith
3
Debo haber estropeado algo con mi intento de perfilar ayer (¡quizás confundió las huellas lentas y rápidas!). Lo he rehecho hoy y, por supuesto , solo muestra lo que ya dijiste.
Martin Smith
2
Sí, la captura de pantalla es el informe de Vista de árbol de llamadas del generador de perfiles de Visual Studio 2012 . Creo que los nombres de los métodos se ven mucho más claros en su salida aunque sin cadenas misteriosas como @@IEAAXPEA_Kaparecer.
Martin Smith
10

Ese es el código de mi artículo publicado originalmente aquí:

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

Si lees los comentarios, encontrarás un par de alternativas que no tienen los problemas de rendimiento que estás experimentando, una que usa una modificación de esa consulta original y la otra que usa una variable para contener el XML antes de procesarlo, lo que funciona mejor. (vea mis comentarios en la página 2) El XML del DMV puede ser lento de procesar, al igual que el análisis del XML del DMF para el destino del archivo, que a menudo se logra mejor leyendo primero los datos en una tabla temporal y luego procesándolos. XML en SQL es lento en comparación con el uso de cosas como .NET o SQLCLR.

Jonathan Kehayias
fuente
1
¡Gracias! Eso hizo el truco. El que no tiene la variable tomando 600ms y 6341 lee y con la variable 303 msy 3249 lob reads. En 2012 también necesitaba agregar and target_name='ring_buffer'a esa versión, ya que parece que ahora hay dos objetivos. Sin embargo, todavía estoy tratando de obtener una imagen mental de lo que está haciendo exactamente en la versión de 20 minutos.
Martin Smith