Home / Databases / SQL Server

DatabasesRunbookIntermediate

SQL Server blocking and deadlock triage

Find the head blocker in seconds, pull deadlock graphs from the system_health session, and pick the right long-term fix, from indexes to RCSI.

Deepak Bhardwaj3 min read

Applies toSQL Server 2016+, Azure SQL Database / Managed Instance, Amazon RDS for SQL Server

Blocking vs deadlock

  • Blocking: session B waits for a lock held by session A. It lasts until A commits or rolls back, which could be forever.
  • Deadlock: A and B each wait for the other. SQL Server detects this within about 5 seconds and kills one of them with error 1205.

Who is blocking right now?

SELECT r.session_id, r.blocking_session_id, r.wait_type, r.wait_time / 1000 AS wait_s,
       r.status, DB_NAME(r.database_id) AS db, s.login_name, s.host_name, s.program_name,
       SUBSTRING(t.text, r.statement_start_offset / 2 + 1,
         (CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END - r.statement_start_offset) / 2 + 1) AS stmt
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON s.session_id = r.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id <> 0
ORDER BY r.wait_time DESC;

The head blocker is a session that blocks others but isn't blocked itself. It often has no current request because it's an open transaction that's sleeping:

SELECT s.session_id, s.status, s.login_name, s.host_name, s.program_name,
       s.last_request_end_time, t.text AS last_sql
FROM sys.dm_exec_sessions s
JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(c.most_recent_sql_handle) t
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 WHERE blocking_session_id <> 0);

Tip

If you can install it, sp_WhoIsActive @find_block_leaders = 1, @get_plans = 1 gives you the same answer with more detail.

Should you kill it?

Before you run KILL <spid>:

  1. Find out what it is. A user's SSMS session with an open transaction is safe to kill. An ETL job or an index rebuild may take a long time to roll back.
  2. Check how much work has been done: sys.dm_tran_database_transactions.database_transaction_log_bytes_used. Rolling back takes about as long as the work did.
  3. After the kill, check progress with KILL <spid> WITH STATUSONLY.

Get the deadlock graphs

The built-in system_health session records them already:

SELECT x.value('(@timestamp)[1]', 'datetime2') AS utc_time,
       x.query('(data/value/deadlock)[1]') AS deadlock_graph
FROM (
  SELECT CAST(event_data AS xml) AS ev
  FROM sys.fn_xe_file_target_read_file('system_health*.xel', NULL, NULL, NULL)
  WHERE object_name = 'xml_deadlock_report'
) d
CROSS APPLY d.ev.nodes('/event') AS e(x)
ORDER BY utc_time DESC;

Save the XML as .xdl and open it in SSMS to see the graph. Note the objects, indexes and statements of both victim and survivor.

Long-term fixes

Pattern Fix
Readers blocked by writers Enable RCSI: ALTER DATABASE app SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; (it's the default on Azure SQL DB)
Scans taking many locks Add a covering index so the query seeks instead of scanning
Two procs updating tables in opposite order Access tables in the same order everywhere
Long transactions Commit in batches, and keep user interaction out of transactions
Lock escalation to table level Batch DML under about 5,000 rows, or ALTER TABLE ... SET (LOCK_ESCALATION = AUTO) for partitioned tables

Warning

RCSI increases tempdb (version store) usage. Turning it on can also change the behavior of code that relies on blocking reads. Test it before you enable it in production.