Los bloqueos en SQL Server son uno de los problemas más habituales —y más frustrantes— con los que nos encontramos los DBAs en el día a día. En este post vamos a ir al grano: scripts y DMVs para detectar bloqueos en SQL Server, identificar la cadena completa y obtener la query causante del problema.
¿Qué es un bloqueo en SQL Server?
Un bloqueo (blocking) en SQL Server ocurre cuando una sesión mantiene un lock sobre un recurso —una fila, una página o una tabla— y otra sesión necesita acceder a ese mismo recurso con un tipo de lock incompatible. La segunda sesión se queda esperando hasta que la primera libere el lock. Es comportamiento esperado del motor, no un error en sí mismo.
El problema aparece cuando esos bloqueos duran demasiado, se encadenan o cuando la sesión bloqueante es una transacción abierta que nadie cerró.
¿Cuál es la diferencia entre un bloqueo y un deadlock en SQL Server?
- Un bloqueo (blocking) es una espera unidireccional: la sesión A retiene un lock y la sesión B espera. B espera indefinidamente hasta que A termine.
- Un deadlock es una espera circular: A espera a B, y B espera a A. SQL Server lo detecta automáticamente y mata a una de las sesiones para romper el ciclo.
Los deadlocks los gestiona el motor. Los bloqueos normales, no: se quedan ahí hasta que alguien intervenga.
¿Cómo saber si hay bloqueos activos en SQL Server?
La forma más rápida de detectar bloqueos activos es consultar la DMV sys.dm_exec_requests. La columna blocking_session_id te indica si una sesión está siendo bloqueada por otra. Si es distinto de 0, hay bloqueo. También puedes usar sys.dm_os_waiting_tasks para obtener más detalle sobre la duración de la espera y el recurso afectado.
El siguiente script combina ambas fuentes para darte una foto completa: sesiones bloqueadas, quién las bloquea, el tipo de espera, el tiempo y la query afectada.
SELECT
r.session_id,
r.status,
r.blocking_session_id,
r.wait_type,
r.wait_time / 1000 AS wait_seconds,
r.wait_resource,
DB_NAME(r.database_id) AS database_name,
s.login_name,
s.host_name,
s.program_name,
t.text AS sql_text
FROM sys.dm_exec_requests AS r
INNER JOIN sys.dm_exec_sessions AS s
ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE r.blocking_session_id <> 0
ORDER BY r.wait_time DESC;| Columna | Qué te cuenta |
|---|---|
session_id | La sesión que está siendo bloqueada |
blocking_session_id | La sesión que causa el bloqueo |
wait_type | El tipo de espera (LCK_M_S, LCK_M_X, LCK_M_U son los más habituales) |
wait_time | Cuánto tiempo lleva esperando |
wait_resource | El recurso concreto por el que espera |
![Resultado de la query mostrando sesiones bloqueadas con wait_type LCK_M_X]](/wp-content/uploads/2026/05/image-1024x94.png)
¿Cómo identificar la query que está causando el bloqueo?
Una vez que sabes qué sesión bloquea, necesitas ver qué está haciendo. El siguiente script obtiene la query de la sesión bloqueante, tanto si tiene una request activa como si es una sesión «sleeping» con una transacción abierta:
DECLARE @blocking_spid INT = 87; -- Pon aquí el blocking_session_id
SELECT
s.session_id,
s.login_name,
s.host_name,
s.program_name,
s.status,
s.last_request_start_time,
r.command,
r.wait_type,
r.open_transaction_count,
t.text AS sql_text,
SUBSTRING(
t.text,
(r.statement_start_offset / 2) + 1,
CASE
WHEN r.statement_end_offset = -1 THEN LEN(t.text)
ELSE (r.statement_end_offset - r.statement_start_offset) / 2
END + 1
) AS current_statement
FROM sys.dm_exec_sessions AS s
LEFT JOIN sys.dm_exec_connections AS c
ON s.session_id = c.session_id
LEFT JOIN sys.dm_exec_requests AS r
ON s.session_id = r.session_id
OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) AS t
WHERE s.session_id = @blocking_spid;Ojo con este detalle: el COALESCE(r.sql_handle, c.most_recent_sql_handle) es clave. Si la sesión bloqueante dejó una transacción abierta pero ya no tiene request activa (aparece como «sleeping»), r.sql_handle será NULL y necesitas most_recent_sql_handle de sys.dm_exec_connections para ver la última query que ejecutó.
Este escenario —sesión sleeping con transacción abierta que bloquea a otras— es uno de los más comunes y de los que más dolores de cabeza dan.
¿Cómo encontrar la cadena completa de bloqueos y el head blocker?
Cuando hay múltiples sesiones bloqueadas, lo habitual es encontrar una cadena de bloqueos: A bloquea a B, que bloquea a C, que bloquea a D. Necesitas encontrar la cabeza de la cadena (head blocker), porque es ahí donde tienes que actuar.
El siguiente script usa un CTE recursivo para recorrer toda la cadena. Incluye el caso de sesiones sleeping con transacciones abiertas:
WITH BlockingChain AS (
-- Head blockers con request activa
SELECT
session_id AS head_blocker,
session_id,
blocking_session_id,
0 AS level
FROM sys.dm_exec_requests
WHERE blocking_session_id = 0
AND session_id IN (
SELECT blocking_session_id
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0
)
UNION ALL
-- Head blockers sleeping (transacción abierta sin request)
SELECT
s.session_id AS head_blocker,
s.session_id,
CAST(0 AS SMALLINT) AS blocking_session_id,
0 AS level
FROM sys.dm_exec_sessions AS s
WHERE s.session_id IN (
SELECT blocking_session_id
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0
)
AND s.session_id NOT IN (
SELECT session_id
FROM sys.dm_exec_requests
)
UNION ALL
-- Recursión
SELECT
bc.head_blocker,
r.session_id,
r.blocking_session_id,
bc.level + 1
FROM sys.dm_exec_requests AS r
INNER JOIN BlockingChain AS bc
ON r.blocking_session_id = bc.session_id
WHERE r.blocking_session_id <> 0
AND bc.level < 20
)
SELECT
bc.head_blocker,
bc.level,
bc.session_id,
s.login_name,
s.host_name,
s.status,
DB_NAME(r.database_id) AS database_name,
t.text AS sql_text,
r.wait_type,
r.wait_time / 1000 AS wait_seconds
FROM BlockingChain AS bc
INNER JOIN sys.dm_exec_sessions AS s
ON bc.session_id = s.session_id
LEFT JOIN sys.dm_exec_requests AS r
ON bc.session_id = r.session_id
LEFT JOIN sys.dm_exec_connections AS c
ON bc.session_id = c.session_id
OUTER APPLY sys.dm_exec_sql_text(
COALESCE(r.sql_handle, c.most_recent_sql_handle)
) AS t
ORDER BY bc.head_blocker, bc.level;La columna level te indica la profundidad en la cadena: 0 es el head blocker, 1 las sesiones directamente bloqueadas por él, y así sucesivamente. Si ves 15 sesiones encadenadas, sabes que es urgente actuar.

¿Cómo resolver un bloqueo en SQL Server?
Una vez identificado el bloqueo, tienes tres opciones:
Esperar. Si la sesión bloqueante es un proceso legítimo a punto de terminar. Monitorízalo con percent_complete si reporta progreso.
Matar la sesión con KILL. Si es una transacción huérfana o un proceso que no debería estar ahí. Cuidado: fuerza un rollback que puede tardar tanto como la operación original.
KILL 87; -- session_id del head blocker
Solucionar la causa raíz. Los bloqueos frecuentes suelen apuntar a: transacciones demasiado largas, falta de índices que provocan table scans en tablas concurridas, nivel de aislamiento inadecuado (READ COMMITTED SNAPSHOT puede resolver muchos casos de contención), o lock escalation no controlado. Si ves locks exclusive a nivel de tabla en sys.dm_tran_locks, probablemente estés ante un caso de escalación.
Las limitaciones de detectar bloqueos con scripts manuales
Todo lo anterior funciona bien, pero tiene limitaciones reales:
No tienes histórico. Las DMVs solo muestran lo que pasa ahora mismo. Si un bloqueo duró 30 segundos hace una hora, ya no hay rastro. Y si ocurrió a las 3 de la mañana, nadie lo va a ver.
No tienes alertas proactivas. Te enteras cuando un usuario te llama, no antes.
No escala. Si gestionas 10 o 50 instancias, no puedes ejecutar scripts en todas manualmente.
No tienes contexto cruzado. No puedes correlacionar el bloqueo con CPU alta, uso de disco o contadores de rendimiento de ese mismo momento.
Cómo Coyote Monitor resuelve la monitorización de bloqueos en SQL Server
Coyote Monitor registra un histórico completo de procesos y bloqueos en todas tus instancias, de forma continua y con impacto mínimo. Cuando un bloqueo ocurre —a cualquier hora—, tienes el detalle completo: sesión bloqueante, sesión bloqueada, queries, duración y recurso afectado. Puedes volver atrás en el tiempo y ver exactamente qué pasó.
Las alertas inteligentes de Coyote Monitor te notifican cuando un bloqueo supera el umbral que configures, y al tener procesos, bloqueos y contadores de rendimiento en un mismo panel, puedes correlacionar todo para llegar a la causa raíz.
👉 Prueba Coyote Monitor gratis durante 30 días
Preguntas frecuentes sobre bloqueos en SQL Server
¿Qué DMV uso para detectar bloqueos en SQL Server?
Las dos DMVs principales son sys.dm_exec_requests (peticiones activas con blocking_session_id) y sys.dm_os_waiting_tasks (tareas en espera con duración y recurso). Combinándolas con sys.dm_exec_sql_text obtienes el texto SQL de las sesiones involucradas.
¿Cómo puedo encontrar el head blocker en una cadena de bloqueos?
El head blocker es la sesión que origina toda la cadena: bloquea a otras pero no está bloqueada por nadie. Se identifica con un CTE recursivo sobre sys.dm_exec_requests. El script completo está en la sección «¿Cómo encontrar la cadena completa de bloqueos?» de este post.
¿Por qué una sesión «sleeping» puede estar causando bloqueos?
Si una sesión abrió una transacción con BEGIN TRAN y no la cerró con COMMIT o ROLLBACK, los locks se mantienen indefinidamente aunque la sesión aparezca como sleeping. Es una de las causas más comunes de bloqueos prolongados en producción.
¿Cuál es la diferencia entre bloqueo y deadlock en SQL Server?
Un bloqueo es una espera unidireccional (A retiene, B espera). Un deadlock es una espera circular (A espera a B y B espera a A). SQL Server resuelve los deadlocks automáticamente matando una sesión. Los bloqueos normales requieren intervención manual.
¿Cómo puedo monitorizar bloqueos en SQL Server de forma continua?
Los scripts manuales solo ofrecen muestreos puntuales sin histórico ni alertas. Para monitorización continua con histórico de bloqueos, alertas proactivas y visibilidad centralizada, necesitas una herramienta como Coyote Monitor para SQL Server, que registra los bloqueos automáticamente en todas tus instancias.

