Como encontrar a consulta que ainda está travando?


15

Consultar o sys.dm_tran_locksDMV mostra quais sessões (SPIDs) estão mantendo bloqueios em recursos como tabela, página e linha.

Para cada bloqueio adquirido, existe alguma maneira de determinar qual instrução SQL (excluir, inserir, atualizar ou selecionar) causou esse bloqueio?

Eu sei que a most_recent_query_handlecoluna do sys.dm_exec_connectionsDMV nos fornece o texto da última consulta executada, mas várias vezes outras consultas foram executadas antes na mesma sessão (SPID) e ainda estão mantendo bloqueios.

Eu já uso o sp_whoisactiveprocedimento (de Adam Machanic) e ele mostra apenas a consulta que está no buffer de entrada no momento (pense DBCC INPUTBUFFER @spid), que nem sempre (e no meu caso geralmente nunca) é a consulta que adquiriu o bloqueio.

Por exemplo:

  1. transação / sessão aberta
  2. exec uma instrução (que mantém um bloqueio em um recurso)
  3. exec outra declaração na mesma sessão
  4. abra outra transação / sessão e tente modificar o recurso bloqueado na etapa 2.

O sp_whoisactiveprocedimento indicará a declaração na etapa 3, que não é responsável pelo bloqueio e, portanto, não é útil.

Essa pergunta veio da análise usando o recurso Relatórios de processos bloqueados , para encontrar a causa raiz dos cenários de bloqueio na produção. Cada transação executa várias consultas e, na maioria das vezes, a última (que é mostrada no buffer de entrada no BPR) raramente é a que mantém a trava.

Tenho uma pergunta de acompanhamento: Estrutura para identificar efetivamente as consultas de bloqueio

Respostas:


15

O SQL Server não mantém um histórico dos comandos que foram executados 1,2 . Você pode determinar quais objetos têm bloqueios, mas não pode necessariamente ver qual instrução causou esses bloqueios.

Por exemplo, se você executar esta instrução:

BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES

E olhe o texto do SQL por meio do identificador sql mais recente, você verá que a instrução aparece. No entanto, se a sessão fez isso:

BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES
GO
SELECT *
FROM dbo.TestLock;
GO

Você só veria a SELECT * FROM dbo.TestLock;instrução, mesmo que a transação não tenha sido confirmada, e a INSERTinstrução esteja bloqueando os leitores contra a dbo.TestLocktabela.

Eu uso isso para procurar transações não confirmadas que estão bloqueando outras sessões:

/*
    This query shows sessions that are blocking other sessions, including sessions that are 
    not currently processing requests (for instance, they have an open, uncommitted transaction).

    By:  Max Vernon, 2017-03-20
*/
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; --reduce possible blocking by this query.

USE tempdb;

IF OBJECT_ID('tempdb..#dm_tran_session_transactions') IS NOT NULL
DROP TABLE #dm_tran_session_transactions;
SELECT *
INTO #dm_tran_session_transactions
FROM sys.dm_tran_session_transactions;

IF OBJECT_ID('tempdb..#dm_exec_connections') IS NOT NULL
DROP TABLE #dm_exec_connections;
SELECT *
INTO #dm_exec_connections
FROM sys.dm_exec_connections;

IF OBJECT_ID('tempdb..#dm_os_waiting_tasks') IS NOT NULL
DROP TABLE #dm_os_waiting_tasks;
SELECT *
INTO #dm_os_waiting_tasks
FROM sys.dm_os_waiting_tasks;

IF OBJECT_ID('tempdb..#dm_exec_sessions') IS NOT NULL
DROP TABLE #dm_exec_sessions;
SELECT *
INTO #dm_exec_sessions
FROM sys.dm_exec_sessions;

IF OBJECT_ID('tempdb..#dm_exec_requests') IS NOT NULL
DROP TABLE #dm_exec_requests;
SELECT *
INTO #dm_exec_requests
FROM sys.dm_exec_requests;

;WITH IsolationLevels AS 
(
    SELECT v.*
    FROM (VALUES 
              (0, 'Unspecified')
            , (1, 'Read Uncomitted')
            , (2, 'Read Committed')
            , (3, 'Repeatable')
            , (4, 'Serializable')
            , (5, 'Snapshot')
        ) v(Level, Description)
)
, trans AS 
(
    SELECT dtst.session_id
        , blocking_sesion_id = 0
        , Type = 'Transaction'
        , QueryText = dest.text
    FROM #dm_tran_session_transactions dtst 
        LEFT JOIN #dm_exec_connections dec ON dtst.session_id = dec.session_id
    OUTER APPLY sys.dm_exec_sql_text(dec.most_recent_sql_handle) dest
)
, tasks AS 
(
    SELECT dowt.session_id
        , dowt.blocking_session_id
        , Type = 'Waiting Task'
        , QueryText = dest.text
    FROM #dm_os_waiting_tasks dowt
        LEFT JOIN #dm_exec_connections dec ON dowt.session_id = dec.session_id
    OUTER APPLY sys.dm_exec_sql_text(dec.most_recent_sql_handle) dest
    WHERE dowt.blocking_session_id IS NOT NULL
)
, requests AS 
(
SELECT des.session_id
    , der.blocking_session_id
    , Type = 'Session Request'
    , QueryText = dest.text
FROM #dm_exec_sessions des
    INNER JOIN #dm_exec_requests der ON des.session_id = der.session_id
OUTER APPLY sys.dm_exec_sql_text(der.sql_handle) dest
WHERE der.blocking_session_id IS NOT NULL
    AND der.blocking_session_id > 0 
)
, Agg AS (
    SELECT SessionID = tr.session_id
        , ItemType = tr.Type
        , CountOfBlockedSessions = (SELECT COUNT(*) FROM requests r WHERE r.blocking_session_id = tr.session_id)
        , BlockedBySessionID = tr.blocking_sesion_id
        , QueryText = tr.QueryText
    FROM trans tr
    WHERE EXISTS (
        SELECT 1
        FROM requests r
        WHERE r.blocking_session_id = tr.session_id
        )
    UNION ALL
    SELECT ta.session_id
        , ta.Type
        , CountOfBlockedSessions = (SELECT COUNT(*) FROM requests r WHERE r.blocking_session_id = ta.session_id)
        , BlockedBySessionID = ta.blocking_session_id
        , ta.QueryText
    FROM tasks ta
    UNION ALL
    SELECT rq.session_id
        , rq.Type
        , CountOfBlockedSessions =  (SELECT COUNT(*) FROM requests r WHERE r.blocking_session_id = rq.session_id)
        , BlockedBySessionID = rq.blocking_session_id
        , rq.QueryText
    FROM requests rq
)
SELECT agg.SessionID
    , ItemType = STUFF((SELECT ', ' + COALESCE(a.ItemType, '') FROM agg a WHERE a.SessionID = agg.SessionID ORDER BY a.ItemType FOR XML PATH ('')), 1, 2, '')
    , agg.BlockedBySessionID
    , agg.QueryText
    , agg.CountOfBlockedSessions
    , des.host_name
    , des.login_name
    , des.is_user_process
    , des.program_name
    , des.status
    , TransactionIsolationLevel = il.Description
FROM agg 
    LEFT JOIN #dm_exec_sessions des ON agg.SessionID = des.session_id
    LEFT JOIN IsolationLevels il ON des.transaction_isolation_level = il.Level
GROUP BY agg.SessionID
    , agg.BlockedBySessionID
    , agg.CountOfBlockedSessions
    , agg.QueryText
    , des.host_name
    , des.login_name
    , des.is_user_process
    , des.program_name
    , des.status
    , il.Description
ORDER BY 
    agg.BlockedBySessionID
    , agg.CountOfBlockedSessions
    , agg.SessionID;

Se configurarmos um banco de testes simples no SSMS com duas janelas de consulta, podemos ver que só podemos ver a instrução ativa mais recente.

Na primeira janela de consulta, execute o seguinte:

CREATE TABLE dbo.TestLock
(
    id int NOT NULL IDENTITY(1,1)
);
BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES

Na segunda janela, execute o seguinte:

SELECT *
FROM  dbo.TestLock

Agora, se executarmos a consulta de transações de bloqueio não confirmada acima, veremos a seguinte saída:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦══════════════════════════════════ ═══════╗
║ SessionID ║ ItemType ║ BlockedBySessionID ║ QueryText ║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬══════════════════════════════════ ═══════╣
║ 67 ║ Transação ║ 0 ║ BEGIN TRANSACTION ║
INSERIR NO dbo.TestLock VALORES PADRÃO
68 Solicitação de sessão, tarefa em espera 67 67 SELECT *
A partir do dbo.TestLock
╚═══════════╩═══════════════════════════════╩═════ ═══════════════╩══════════════════════════════════ ═══════╝

(Removi algumas colunas irrelevantes do final dos resultados).

Agora, se mudarmos a primeira janela de consulta para isso:

BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES
GO
SELECT *
FROM dbo.TestLock;
GO

e execute novamente a 2ª janela de consulta:

SELECT *
FROM  dbo.TestLock

Veremos essa saída da consulta de transações de bloqueio:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦════════════════════╗
║ SessionID ║ ItemType ║ BlockedBySessionID ║ QueryText ║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬════════════════════╣
║ 67 ║ Transação ║ 0 ║ SELECT * ║
D ║ ║ ║ FROM dbo.TestLock; ║
68 Solicitação de sessão, tarefa em espera 67 67 SELECT *
A partir do dbo.TestLock
╚═══════════╩═══════════════════════════════╩═════ ═══════════════╩════════════════════╝

1 - não é inteiramente verdade. Existe o cache do procedimento, que pode conter a instrução responsável pelo bloqueio. No entanto, pode não ser fácil determinar qual instrução é a causa real do bloqueio, pois pode haver muitas consultas no cache que tocam no recurso em questão.

A consulta abaixo mostra o plano de consulta para as consultas de teste acima, pois meu cache de procedimento não está muito ocupado.

SELECT TOP(30) t.text
    , p.query_plan
    , deqs.execution_count
    , deqs.total_elapsed_time
    , deqs.total_logical_reads
    , deqs.total_logical_writes
    , deqs.total_logical_writes
    , deqs.total_rows
    , deqs.total_worker_time
    , deqs.*
FROM sys.dm_exec_query_stats deqs
OUTER APPLY sys.dm_exec_sql_text(deqs.sql_handle) t 
OUTER APPLY sys.dm_exec_query_plan(deqs.plan_handle) p
WHERE t.text LIKE '%dbo.TestLock%'  --change this to suit your needs
    AND t.text NOT LIKE '/\/\/\/\/EXCLUDE ME/\/\/\/\/\'
ORDER BY 
    deqs.total_worker_time DESC;

Os resultados dessa consulta podem permitir que você encontre o culpado, mas esteja ciente de que a inspeção do cache do procedimento como essa pode ser bastante exigente em um sistema ocupado.

2 SQL Server 2016 e oferta acima da Consulta loja , que faz manter o histórico completo de consultas executadas.


Obrigado @Max, muito bem explicado. Essa dúvida surgiu durante a análise do Blocked Process Reportsrecurso, para encontrar a causa raiz dos cenários de bloqueio na produção. Cada transação executa várias consultas e, na maioria das vezes, a última (que é mostrada no buffer de entrada no BPR) raramente é a que mantém a trava. Parece que meu último recurso para resolver isso é definir uma sessão leve do xEvents para me dizer quais consultas foram executadas em cada sessão. Se você conhece um artigo mostrando um exemplo disso, ficarei grato.
— 22418 tanitelle

Também em relação ao Query Store, é muito útil, mas não possui as informações do SPID. Obrigado mesmo assim.
— tanitelle


6

Para complementar a resposta de Max , encontrei abaixo utilitários extremamente úteis:

Uso o beta_lockinfo quando quero aprofundar o bloqueio e analisar o que e como o bloqueio surgiu - o que é extremamente útil.

beta_lockinfo é um procedimento armazenado que fornece informações sobre processos e bloqueios que eles mantêm, bem como suas transações ativas. O beta_lockinfo foi projetado para reunir o máximo de informações possível sobre uma situação de bloqueio, para que você possa encontrar instantaneamente o culpado e matar o processo de bloqueio, se a situação estiver desesperada. Em seguida, você pode relaxar e analisar a saída de beta_lockinfo para entender como surgiu a situação de bloqueio e descobrir quais ações executar para impedir que a situação se repita. A saída de beta_lockinfo mostra todos os processos ativos e passivos com bloqueios, quais objetos eles bloqueiam, qual comando eles enviaram pela última vez e qual instrução eles estão executando. Você também obtém os planos de consulta para as instruções atuais.


1
uau, esse proc de Erland Sommarskog é incrível.
— Max Vernon

1
Sim .. eu uso quando tenho que mergulhar fundo nos detalhes de bloqueio.
— Kin Shah
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.