Executando a consulta daqui para retirar os eventos de impasse da sessão padrão de eventos estendidos
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';
leva cerca de 20 minutos para ser concluído 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.
Se eu remover a WHERE
clá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 acelera as coisas também, 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.
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 JOIN
melhorar as coisas até certo ponto (mas ainda leva> 3 minutos), pois os resultados do DMV são tão pequenos que duvido que o próprio tipo Join seja 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.
Depois de preencher o buffer de anel de eventos estendidos ( DATALENGTH
dos XML
quais eram 4.880.045 bytes e continha 1.448 eventos.) e testar uma versão reduzida da consulta original com e sem a MAXDOP
dica.
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 de tempdb com a mais rápida mostrando que as 616
páginas foram alocadas e desalocadas. Esta é 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ágina chegam a milhões. A pesquisa dm_db_task_space_usage
enquanto a consulta está em execução mostra que parece estar constantemente alocando e desalocando páginas tempdb
entre 1.800 e 3.000 páginas alocadas a qualquer momento.
O motivo da diferença de desempenho está em como as expressões escalares são tratadas no mecanismo de execução. Neste caso, a manifestação de interesse é:
Esse rótulo de expressão é definido por um operador Compute Scalar (nó 11 no plano serial, nó 13 no plano paralelo). Os operadores Compute Scalar são diferentes de outros operadores (SQL Server 2005 em diante) porque 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 do cálculo seja solicitado por um operador posterior.
Na consulta atual, a
target_data
string geralmente é grande, tornando a conversão de string emXML
dispendiosa. Em planos lentos, a string paraXML
conversão é executada toda vez que um operador posterior que requer o resultadoExpr1000
é rebote.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 das junções de loops aninhados neste plano de execução. A expressão é referenciada várias vezes por vários leitores XML, ambos Stream Aggregates e por um filtro de inicialização. Dependendo do tamanho doXML
, o número de vezes que a string é convertidaXML
pode facilmente chegar a milhões.As pilhas de chamadas abaixo mostram exemplos da
target_data
string sendo convertida paraXML
(ConvertStringToXMLForES
- onde ES é o Serviço de Expressão ):Filtro de inicialização
Leitor de XML (TVF Stream internamente)
Fluxo Agregado
A conversão da string para
XML
cada vez que qualquer um desses operadores é religado explica a diferença de desempenho observada com os planos de loops aninhados. Isso ocorre independentemente de o paralelismo ser usado ou não. Acontece que o otimizador escolhe um hash join quando aMAXDOP 1
dica é especificada. SeMAXDOP 1, LOOP JOIN
for 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
Expr1000
aparece no lado de compilação ou de teste do operador. A consulta a seguir localiza a expressão no lado da sonda:Inverti a ordem escrita das junções da versão mostrada na pergunta, porque as dicas de junção (
INNER HASH JOIN
acima) também forçam a ordem para toda a consulta, como seFORCE ORDER
tivesse sido especificada. A inversão é necessária para garantir queExpr1000
apareça no lado da sonda. A parte interessante do plano de execução é:Com a expressão definida no lado da sonda, o valor é armazenado em cache:
A avaliação de
Expr1000
ainda é 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 leitores de XML e agregados de fluxo. O rastreamento de pilha abaixo mostra um exemplo do valor armazenado em cache sendo reutilizado por um leitor de XML.Quando a ordem de junção é forçada de forma que a definição de
Expr1000
ocorra no lado da construção da junção hash, a situação é diferente:Uma junção de hash lê sua entrada de compilação completamente para construir uma tabela de hash antes de iniciar a sondagem de correspondências. Como resultado, temos que armazenar todos os valores, não apenas aquele por thread que está sendo trabalhado do lado da sonda do plano. O hash join, portanto, usa uma
tempdb
mesa de trabalho para armazenar osXML
dados, e todo acesso ao resultadoExpr1000
por operadores posteriores requer uma viagem cara paratempdb
:O seguinte mostra mais detalhes do caminho de acesso lento:
If a merge join is forced the input rows are sorted (a blocking operation, just like the build input to a hash join) resulting in a similar arrangement where slow access via a
tempdb
sort-optimized worktable is required because of the size of the data.Plans that manipulate large data items can be problematic for all sorts of reasons that are not apparent from the execution plan. Using a hash join (with the expression on the correct input) is not a good solution. It relies on undocumented internal behaviour with no guarantees it will work the same way next week, or on a slightly different query.
A mensagem é que a
XML
manipulação pode ser algo complicado de otimizar hoje. Escrever oXML
em uma variável ou tabela temporária antes de triturar é uma solução alternativa muito mais sólida do que qualquer coisa mostrada acima. Uma maneira de fazer isso é:Finalmente, eu só quero adicionar o gráfico muito bom de Martin dos comentários abaixo:
Esse é o código do meu artigo originalmente postado aqui:
http://www.sqlservercentral.com/articles/deadlock/65658/
Se você ler os comentários, encontrará algumas alternativas que não têm 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, o que funciona Melhor. (veja meus comentários na página 2) O XML do DMV pode ser lento para processar, assim como a análise do XML do DMF para o destino do arquivo, que geralmente é melhor realizado lendo os dados em uma tabela temporária primeiro e depois processando-os. XML em SQL é lento em comparação com coisas como .NET ou SQLCLR.