Otimizando planos com leitores XML

34

Executando a consulta daqui para extrair os eventos de deadlock da sessão de eventos estendidos padrão

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

demora cerca de 20 minutos a concluir na minha máquina. As estatísticas relatadas são

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 do plano lento

Paralelo

Se eu remover a WHEREcláusula, ela será concluída em menos de um segundo, retornando 3.782 linhas.

Da mesma forma, se eu adicionar OPTION (MAXDOP 1)à consulta original que também acelera as coisas, as estatísticas agora mostram muito menos leituras de 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 de plano mais rápido

Serial

Então minha pergunta é

Alguém pode explicar o que está acontecendo? Por que o plano original é tão catastroficamente pior e existe alguma maneira confiável de evitar o problema?

Adição:

Também descobri que alterar a consulta para INNER HASH JOINmelhorar as coisas até certo ponto (mas ainda leva mais de 3 minutos), pois os resultados do DMV são tão pequenos que duvido que o tipo Join seja o responsável, e presumo que algo mais deva ter mudado. Estatísticas para isso

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 plano)

Depois de encher o tampão anel eventos alargados DATALENGTH(da XMLfoi 4,880,045 bytes e continha 1.448 eventos.) E teste de um corte a versão da consulta original com e sem a MAXDOPdica.

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 

Deu os seguintes 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 |
+-------------------------------------+------+----------+

Há uma clara diferença nas alocações tempdb, sendo que a mais rápida mostra as 616páginas que foram alocadas e desalocadas. Essa é a mesma quantidade de páginas usadas quando o XML também é colocado em uma variável.

Para o plano lento, essas contagens de alocação de páginas estão na casa dos milhões. A pesquisa dm_db_task_space_usageenquanto a consulta está em execução mostra que parece estar constantemente alocando e desalocando páginas tempdbcom algo entre 1.800 e 3.000 páginas alocadas ao mesmo tempo.

Martin Smith
fonte
Você pode mover a WHEREcláusula para a expressão XQuery; a lógica não tem que ser removido para que ele vá rápido: TargetData.nodes ('RingBufferTarget[1]/event[@name = "xml_deadlock_report"]'). Dito isto, não conheço elementos internos XML suficientemente bem para responder à pergunta que você fez.
precisa
Paginando o @SQLPoolBoy para você Martin ... ele sugeriu passar pelos comentários aqui, onde ele tem sugestões mais eficientes (elas são baseadas no artigo de origem do código acima ).
Aaron Bertrand

Respostas:

36

O motivo da diferença de desempenho está na maneira como as expressões escalares são tratadas no mecanismo de execução. Nesse caso, a manifestação de interesse é:

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

Esse rótulo de expressão é definido por um operador Compute Scalar (nó 11 no plano serial, nó 13 no plano paralelo). Os operadores escalares de computação são diferentes de outros operadores (SQL Server 2005 em diante), pois as expressões que eles definem não são necessariamente avaliadas na posição em que aparecem no plano de execução visível; a avaliação pode ser adiada até que o resultado da computação seja requerido por um operador posterior.

Na presente consulta, a target_datastring é tipicamente grande, tornando a conversão de string em XMLcara. Em planos lentos, a sequência de XMLconversão é executada toda vez que um operador posterior que requer o resultado de Expr1000é recuperado.

A religação ocorre no lado interno de uma junção de loops aninhados quando um parâmetro correlacionado (referência externa) é alterado. Expr1000é uma referência externa para a maioria dos loops aninhados neste plano de execução. A expressão é referenciada várias vezes por vários leitores XML, agregados de fluxo e por um filtro de inicialização. Dependendo do tamanho do XML, o número de vezes que a string é convertida XMLpode ser facilmente numerado em milhões.

As pilhas de chamadas abaixo mostram exemplos da target_datasequência que está sendo convertida para XML( ConvertStringToXMLForES- onde ES é o Serviço de Expressão ):

Filtro de inicialização

Pilha de chamadas do filtro de inicialização

Leitor XML (TVF Stream internamente)

Pilha de chamadas de TVF Stream

Agregar fluxo

Pilha de chamadas agregadas de fluxo

A conversão da cadeia de caracteres para XMLcada vez que um desses operadores religar explica a diferença de desempenho observada nos planos de loops aninhados. Isso independentemente de o paralelismo ser usado ou não. Acontece que o otimizador escolhe uma junção de hash quando a MAXDOP 1dica é especificada. Se MAXDOP 1, LOOP JOINfor especificado, o desempenho será ruim, assim como no plano paralelo padrão (onde o otimizador escolhe loops aninhados).

Quanto o desempenho aumenta com uma junção de hash depende se Expr1000aparece no lado da construção ou da sonda do operador. A consulta a seguir localiza a expressão no lado do probe:

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

Inverti a ordem por escrito das junções da versão mostrada na pergunta, porque as dicas de junção ( INNER HASH JOINacima) também forçam a ordem para toda a consulta, como se FORCE ORDERtivesse sido especificada. A reversão é necessária para garantir que Expr1000apareça no lado da sonda. A parte interessante do plano de execução é:

dica 1

Com a expressão definida no lado da análise, o valor é armazenado em cache:

Cache de hash

A avaliação de Expr1000ainda é adiada até que o primeiro operador precise do valor (o filtro de inicialização no rastreamento de pilha acima), mas o valor calculado é armazenado em cache ( CValHashCachedSwitch) e reutilizado para chamadas posteriores pelos XML Readers e Stream Aggregates. O rastreamento de pilha abaixo mostra um exemplo do valor em cache sendo reutilizado por um XML Reader.

Reutilização de cache

Quando a ordem de junção é forçada de modo que a definição de Expr1000ocorra no lado da construção da junção de hash, a situação é 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

Uma junção de hash lê sua entrada de construção completamente para construir uma tabela de hash antes de começar a pesquisar correspondências. Como resultado, precisamos armazenar todos os valores, não apenas o valor por thread trabalhado no lado da sonda do plano. A junção de hash, portanto, usa uma tempdbtabela de trabalho para armazenar os XMLdados, e todo acesso ao resultado de Expr1000operadores posteriores exige uma viagem cara para tempdb:

Acesso lento

A seguir, são mostrados mais detalhes do caminho de acesso lento:

Detalhes lentos

Se uma junção de mesclagem for forçada, as linhas de entrada serão classificadas (uma operação de bloqueio, assim como a entrada de compilação em uma junção de hash), resultando em uma organização semelhante em que o acesso lento por meio de uma tempdbmesa de trabalho otimizada por classificação é necessário devido ao tamanho dos dados.

Os planos que manipulam grandes itens de dados podem ser problemáticos por todos os tipos de razões que não são aparentes no plano de execução. Usar uma junção de hash (com a expressão na entrada correta) não é uma boa solução. Ele se baseia em um comportamento interno não documentado, sem garantias de que funcionará da mesma maneira na próxima semana ou em uma consulta ligeiramente diferente.

A mensagem é que a XMLmanipulação pode ser algo complicado de otimizar hoje. Gravar XMLem uma tabela variável ou temporária antes da destruição é uma solução muito mais sólida do que qualquer coisa mostrada acima. Uma maneira de fazer isso é:

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, só quero adicionar um gráfico muito bom de Martin a partir dos comentários abaixo:

Gráfico de Martin

Paul White diz que a GoFundMonica
fonte
Ótima explicação, obrigado. Também li seu artigo sobre escalares de computação, mas não coloquei dois e dois juntos aqui.
Martin Smith
3
Eu devo ter estragado alguma coisa com a minha tentativa de criar perfis ontem (talvez confundir traços lentos e rápidos!). Eu o refiz hoje e, claro , apenas mostra o que você já disse.
Martin Smith
2
Sim, a captura de tela é o relatório do modo de exibição de árvore de chamadas do criador de perfil do Visual Studio 2012 . Eu acho que os nomes dos métodos parecem muito mais claros em sua saída, embora sem sequências misteriosas, como a @@IEAAXPEA_Kexibição.
Martin Smith
10

Esse é o código do meu artigo publicado originalmente aqui:

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

Se você ler os comentários, encontrará algumas alternativas que não apresentam os problemas de desempenho que você está enfrentando, uma usando uma modificação dessa consulta original e a outra usando uma variável para armazenar o XML antes de processá-lo, que funciona. Melhor. (veja meus comentários na página 2) O XML das DMVs pode demorar para processar, assim como a análise de XML da DMF para o destino do arquivo, que geralmente é melhor realizada lendo os dados em uma tabela temporária e depois processando-os. O XML no SQL é lento em comparação ao uso de coisas como .NET ou SQLCLR.

Jonathan Kehayias
fonte
1
Obrigado! Isso fez o truque. Aquele sem a variável tomando 600ms e 6341 lê e com a variável 303 mse 3249 lob reads. Em 2012, eu também precisava adicionar and target_name='ring_buffer'a essa versão, pois parece que existem dois destinos agora. Ainda estou tentando ter uma imagem mental do que exatamente está fazendo na versão de 20 minutos.
Martin Smith