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.
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 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.
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 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.
Depois de encher o tampão anel eventos alargados DATALENGTH
(da XML
foi 4,880,045 bytes e continha 1.448 eventos.) E teste de um corte a versão 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 tempdb, sendo que a mais rápida mostra as 616
pá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_usage
enquanto a consulta está em execução mostra que parece estar constantemente alocando e desalocando páginas tempdb
com algo entre 1.800 e 3.000 páginas alocadas ao mesmo tempo.
fonte
WHERE
clá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.Respostas:
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 é:
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_data
string é tipicamente grande, tornando a conversão de string emXML
cara. Em planos lentos, a sequência deXML
conversão é executada toda vez que um operador posterior que requer o resultado deExpr1000
é 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 doXML
, o número de vezes que a string é convertidaXML
pode ser facilmente numerado em milhões.As pilhas de chamadas abaixo mostram exemplos da
target_data
sequência que está sendo convertida paraXML
(ConvertStringToXMLForES
- onde ES é o Serviço de Expressão ):Filtro de inicialização
Leitor XML (TVF Stream internamente)
Agregar fluxo
A conversão da cadeia de caracteres para
XML
cada 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 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 da construção ou da sonda do operador. A consulta a seguir localiza a expressão no lado do probe:Inverti a ordem por escrito 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 reversã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 análise, 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 XML Readers e Stream Aggregates. O rastreamento de pilha abaixo mostra um exemplo do valor em cache sendo reutilizado por um XML Reader.Quando a ordem de junção é forçada de modo que a definição de
Expr1000
ocorra no lado da construção da junção de hash, a situação é diferente: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
tempdb
tabela de trabalho para armazenar osXML
dados, e todo acesso ao resultado deExpr1000
operadores posteriores exige uma viagem cara paratempdb
:A seguir, são mostrados mais detalhes do caminho de acesso lento:
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
tempdb
mesa 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
XML
manipulação pode ser algo complicado de otimizar hoje. GravarXML
em 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 é:Finalmente, só quero adicionar um gráfico muito bom de Martin a partir dos comentários abaixo:
fonte
@@IEAAXPEA_K
exibição.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.
fonte
303 ms
e3249 lob reads
. Em 2012, eu também precisava adicionarand 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.