¿Cómo encontrar la consulta que aún mantiene un bloqueo?

15

Consultar el sys.dm_tran_locksDMV nos muestra qué sesiones (SPID) mantienen bloqueos en recursos como tabla, página y fila.

Para cada bloqueo adquirido, ¿hay alguna forma de determinar qué instrucción SQL (eliminar, insertar, actualizar o seleccionar) causó ese bloqueo?

Sé que la most_recent_query_handlecolumna del sys.dm_exec_connectionsDMV nos da el texto de la última consulta ejecutada, pero varias veces otras consultas se ejecutaron antes en la misma sesión (SPID) y todavía tienen bloqueos.

Ya utilizo el sp_whoisactiveprocedimiento (de Adam Machanic) y solo muestra la consulta que está en el búfer de entrada en este momento (pensar DBCC INPUTBUFFER @spid), que no siempre (y en mi caso generalmente nunca) es la consulta que adquirió el bloqueo.

Por ejemplo:

  1. abrir transacción / sesión
  2. ejecutar una declaración (que mantiene un bloqueo en un recurso)
  3. exec otra declaración sobre la misma sesión
  4. abra otra transacción / sesión e intente modificar el recurso bloqueado en el paso 2.

El sp_whoisactiveprocedimiento señalará la declaración en el paso 3, que no es responsable del bloqueo y, por lo tanto, no es útil.

Esta pregunta vino de hacer un análisis usando la función de Informes de proceso bloqueado , para encontrar la causa raíz de los escenarios de bloqueo en la producción. Cada transacción ejecuta varias consultas, y la mayoría de las veces la última (que se muestra en el búfer de entrada en BPR) rara vez es la que mantiene el bloqueo.

Tengo una pregunta de seguimiento: Marco para identificar efectivamente las consultas de bloqueo

tanitelle
fuente

Respuestas:

15

SQL Server no mantiene un historial de los comandos que se han ejecutado 1,2 . Puede determinar qué objetos tienen bloqueos, pero no necesariamente puede ver qué enunciado causó esos bloqueos.

Por ejemplo, si ejecuta esta declaración:

BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES

Y mire el Texto SQL a través del controlador sql más reciente, verá que la declaración aparece. Sin embargo, si la sesión hizo esto:

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

Solo vería la SELECT * FROM dbo.TestLock;declaración, aunque la transacción no se haya confirmado, y la INSERTdeclaración está bloqueando a los lectores contra la dbo.TestLockmesa.

Lo uso para buscar transacciones no confirmadas que bloquean otras sesiones:

/*
    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;

Si configuramos un banco de pruebas simple en SSMS con un par de ventanas de consulta, podemos ver que solo podemos ver la declaración activa más reciente.

En la primera ventana de consulta, ejecute esto:

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

En la segunda ventana, ejecuta esto:

SELECT *
FROM  dbo.TestLock

Ahora, si ejecutamos la consulta de transacciones de bloqueo no confirmadas desde arriba, vemos el siguiente resultado:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦══════════════════════════════════ ═══════╗
║ SessionID ║ ItemType ║ BlockedBySessionID ║ QueryText ║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬══════════════════════════════════ ═══════╣
║ 67 ║ Transacción ║ 0 ║ COMENZAR TRANSACCIÓN ║
║ ║ ║ ║ INSERTAR EN dbo.TestLock VALORES POR DEFECTO ║
║ 68 ║ Solicitud de sesión, Tarea en espera ║ 67 ║ SELECCIONAR * ║
║ ║ ║ ║ DESDE dbo.TestLock ║
╚═══════════╩═══════════════════════════════╩═════ ═══════════════╩══════════════════════════════════ ═══════╝

(He eliminado algunas columnas irrelevantes del final de los resultados).

Ahora, si cambiamos la primera ventana de consulta a esto:

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

y vuelva a ejecutar la segunda ventana de consulta:

SELECT *
FROM  dbo.TestLock

Veremos esta salida de la consulta de transacciones de bloqueo:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦════════════════════╗
║ SessionID ║ ItemType ║ BlockedBySessionID ║ QueryText ║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬════════════════════╣
║ 67 ║ Transacción ║ 0 ║ SELECCIONAR * ║
║ ║ ║ ║ DESDE dbo.TestLock; ║
║ 68 ║ Solicitud de sesión, Tarea en espera ║ 67 ║ SELECCIONAR * ║
║ ║ ║ ║ DESDE dbo.TestLock ║
╚═══════════╩═══════════════════════════════╩═════ ═══════════════╩════════════════════╝

1 - no del todo cierto. Existe el caché de procedimientos, que puede contener la declaración responsable del bloqueo. Sin embargo, puede que no sea fácil determinar qué enunciado es la causa real del bloqueo, ya que puede haber muchas consultas en la memoria caché que tocan el recurso en cuestión.

La consulta a continuación muestra el plan de consulta para las consultas de prueba anteriores ya que mi caché de procedimientos no está muy ocupada.

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;

Los resultados de esta consulta pueden permitirle encontrar al culpable, pero tenga en cuenta que inspeccionar el caché de procedimientos de esta manera puede ser bastante exigente en un sistema ocupado.

2 de SQL Server 2016 y por encima de la oferta de la tienda de consultas , lo que hace conservar la historia completa de consultas ejecutadas.

Max Vernon
fuente
Gracias @Max, muy bien explicado. Esta duda nació al hacer un análisis de Blocked Process Reportscaracterísticas, para encontrar la causa raíz de los escenarios de bloqueo en la producción. Cada transacción ejecuta varias consultas, y la mayoría de las veces la última (que se muestra en el búfer de entrada en BPR) rara vez es la que mantiene el bloqueo. Parece que mi último recurso para resolver esto es establecer una sesión ligera de xEvents para decirme qué consultas se ejecutaron en cada sesión. Si conoces un artículo que muestre un ejemplo de esto, te lo agradeceré.
tanitelle
También con respecto al Almacén de consultas, es muy útil, pero carece de la información SPID. Gracias de cualquier manera.
tanitelle
más o menos un duplicado de dba.stackexchange.com/questions/187794/…
Mitch Wheat
6

Para complementar la respuesta de Max , he encontrado las siguientes utilidades extremadamente útiles:

Utilizo beta_lockinfo cuando quiero profundizar en el bloqueo y analizar qué y cómo surgió el bloqueo, lo cual es extremadamente útil.

beta_lockinfo es un procedimiento almacenado que proporciona información sobre los procesos y los bloqueos que contienen, así como sus transacciones activas. beta_lockinfo está diseñado para recopilar tanta información sobre una situación de bloqueo como sea posible, de modo que pueda encontrar instantáneamente al culpable y matar el proceso de bloqueo si la situación es desesperada. Luego puede sentarse y analizar el resultado de beta_lockinfo para comprender cómo surgió la situación de bloqueo y determinar qué acciones tomar para evitar que la situación vuelva a ocurrir. El resultado de beta_lockinfo muestra todos los procesos activos y pasivos con bloqueos, qué objetos bloquean, qué comando enviaron por última vez y qué declaración están ejecutando. También obtiene los planes de consulta para las declaraciones actuales.

Kin Shah
fuente
1
wow, ese proceso de Erland Sommarskog es increíble.
Max Vernon
1
Sí. Lo uso cuando tengo que profundizar en los detalles de bloqueo.
Kin Shah