Deadlocks and Blocking in SQL Server
- BLOCKING: Session B waits because Session A holds a lock B needs. Normal and temporary. Becomes a problem when A holds locks too long.
DEADLOCK : A waits for B AND B waits for A -> nobody can continue.
SQL Server detects it (every ~5 sec), kills the cheaper one
("deadlock victim") with error 1205, the other continues.
Session A: locks Row 1 -----> wants Row 2 (held by B)
Session B: locks Row 2 -----> wants Row 1 (held by A)
HOW TO PRACTICE: Open TWO (or three) query windows. Run PART A once.
Then follow the steps in the commented blocks.
Run the sample database script first. Every query below was run on that data, and the Output under each query is the real result.
USE SqlLearningDb;
GO
Part A: Setup (run once)
DROP TABLE IF EXISTS dbo.WarehouseStock;
DROP TABLE IF EXISTS dbo.CustomerWallet;
CREATE TABLE dbo.WarehouseStock
(
ItemID INT PRIMARY KEY,
ItemName VARCHAR(50) NOT NULL,
Qty INT NOT NULL
);
INSERT INTO dbo.WarehouseStock (ItemID, ItemName, Qty) VALUES
(1, 'Laptop', 50), (2, 'Mouse', 500), (3, 'Keyboard', 300), (4, 'Monitor', 80);
CREATE TABLE dbo.CustomerWallet
(
WalletID INT PRIMARY KEY,
CustomerID INT NOT NULL,
Balance DECIMAL(12,2) NOT NULL
);
INSERT INTO dbo.CustomerWallet (WalletID, CustomerID, Balance) VALUES
(1, 1, 100000), (2, 2, 50000), (3, 3, 25000);
GO
Part B: CREATE Blocking
Run these steps in two query windows in SSMS (Window 1 and Window 2), one step at a time, to see how the sessions affect each other.
-- WINDOW 1 : open transaction and keep it open (do NOT commit yet)
BEGIN TRAN;
UPDATE dbo.WarehouseStock SET Qty = Qty - 1 WHERE ItemID = 1;
-- WINDOW 2 : this waits (spinning icon "Executing query...")
SELECT * FROM dbo.WarehouseStock WHERE ItemID = 1;
-- WINDOW 3 : find who is blocking whom -> run PART C queries here
-- WINDOW 1 : finish, and window 2 continues immediately
COMMIT;
Part C: Diagnose Blocking (run these while Part B is blocked)
Who is blocked and by whom
SELECT r.session_id AS BlockedSession,
r.blocking_session_id AS BlockingSession,
r.wait_type,
r.wait_time / 1000.0 AS WaitSeconds,
r.status,
t.text AS BlockedQuery
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id <> 0;
Output
| BlockedSession | BlockingSession | wait_type | WaitSeconds | status | BlockedQuery |
|---|
(0 rows)
What the Blocking session last ran (it may be idle with an open transaction)
SELECT s.session_id, s.login_name, s.host_name, s.status, s.open_transaction_count,
t.text AS LastQuery
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);
Output
| session_id | login_name | host_name | status | open_transaction_count | LastQuery |
|---|
(0 rows)
Locks currently held / requested in this database
SELECT l.request_session_id AS SessionID,
l.resource_type, l.resource_description,
l.request_mode AS LockMode, -- S, X, U, IX, IS ...
l.request_status AS Status, -- GRANT or WAIT
OBJECT_NAME(p.object_id) AS TableName
FROM sys.dm_tran_locks l
LEFT JOIN sys.partitions p ON p.hobt_id = l.resource_associated_entity_id
WHERE l.resource_database_id = DB_ID()
ORDER BY l.request_session_id;
Output
| SessionID | resource_type | resource_description | LockMode | Status | TableName |
|---|---|---|---|---|---|
| 54 | DATABASE | ... | S | GRANT | NULL |
Quick classic tool
EXEC sp_who2;
Output
| SPID | Status | Login | HostName | BlkBy | DBName | Command | CPUTime | DiskIO | LastBatch | ProgramName | SPID | REQUESTID |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 1 | 0 | |
| 2 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 2 | 0 | |
| 3 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 3 | 0 | |
| 4 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 4 | 0 | |
| 5 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 5 | 0 | |
| 6 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 6 | 0 | |
| 7 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 7 | 0 | |
| 8 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 8 | 0 | |
| 9 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 9 | 0 | |
| 10 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 10 | 0 | |
| 11 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 11 | 0 | |
| 12 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 12 | 0 | |
| 13 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 13 | 0 | |
| 14 | BACKGROUND | sa | . | . | NULL | PARALLEL REDO TASK | 0 | 0 | 10/18 18:21:57 | 14 | 0 | |
| 15 | BACKGROUND | sa | . | . | NULL | XIO_AADTOKEN_RENEWAL_TASK | 0 | 0 | 10/18 18:21:57 | 15 | 0 |
... showing 15 of 88 rows
Last resort: kill the blocking session (it will Roll Back its work)
KILL 57; -- replace 57 with the BlockingSession number
Part D: CREATE a Deadlock
Run these steps in two query windows in SSMS (Window 1 and Window 2), one step at a time, to see how the sessions affect each other.
-- WINDOW 1 (step 1)
BEGIN TRAN;
UPDATE dbo.WarehouseStock SET Qty = Qty - 1 WHERE ItemID = 1; -- locks item 1
-- WINDOW 2 (step 2)
BEGIN TRAN;
UPDATE dbo.WarehouseStock SET Qty = Qty - 1 WHERE ItemID = 2; -- locks item 2
-- WINDOW 1 (step 3) : waits for item 2
UPDATE dbo.WarehouseStock SET Qty = Qty - 1 WHERE ItemID = 2;
-- WINDOW 2 (step 4) : waits for item 1 ==> DEADLOCK!
UPDATE dbo.WarehouseStock SET Qty = Qty - 1 WHERE ItemID = 1;
Within ~5 seconds one window shows:
Msg 1205 ... Transaction (Process ID xx) was deadlocked on lock resources with
another process and has been chosen as the deadlock victim. Rerun the transaction.
In the OTHER window run: COMMIT;
Part E: Read the Deadlock Graph (system_health Extended Event, always on)
SELECT TOP (5)
xed.value('@timestamp', 'datetime2') AS DeadlockTime,
xed.query('.') AS DeadlockGraphXml -- click it in SSMS, save as .xdl to view the graph
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 s.name = 'system_health' AND st.target_name = 'ring_buffer'
) AS data
CROSS APPLY TargetData.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS x(xed)
ORDER BY DeadlockTime DESC;
GO
Output
| DeadlockTime | DeadlockGraphXml |
|---|
(0 rows)
Part F: How to Avoid Deadlocks
Access objects in the Same Order everywhere (e.g. always lower ItemID first)
CREATE OR ALTER PROCEDURE dbo.usp_MoveStock
@FromItem INT, @ToItem INT, @Qty INT
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @First INT = IIF(@FromItem < @ToItem, @FromItem, @ToItem),
@Second INT = IIF(@FromItem < @ToItem, @ToItem, @FromItem);
BEGIN TRAN;
-- lock both rows in a fixed order (UPDLOCK = "I'm going to update this")
SELECT Qty FROM dbo.WarehouseStock WITH (UPDLOCK, ROWLOCK) WHERE ItemID = @First;
SELECT Qty FROM dbo.WarehouseStock WITH (UPDLOCK, ROWLOCK) WHERE ItemID = @Second;
UPDATE dbo.WarehouseStock SET Qty = Qty - @Qty WHERE ItemID = @FromItem;
UPDATE dbo.WarehouseStock SET Qty = Qty + @Qty WHERE ItemID = @ToItem;
COMMIT TRAN;
END;
GO
Retry automatically when chosen as deadlock victim (error 1205)
CREATE OR ALTER PROCEDURE dbo.usp_MoveStockWithRetry
@FromItem INT, @ToItem INT, @Qty INT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Retry INT = 3;
WHILE @Retry > 0
BEGIN
BEGIN TRY
EXEC dbo.usp_MoveStock @FromItem, @ToItem, @Qty;
SET @Retry = 0; -- success -> exit loop
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRAN;
IF ERROR_NUMBER() = 1205 AND @Retry > 1
BEGIN
SET @Retry -= 1;
WAITFOR DELAY '00:00:00.200'; -- small back-off
END
ELSE
THROW;
END CATCH;
END;
END;
GO
EXEC dbo.usp_MoveStockWithRetry @FromItem = 2, @ToItem = 1, @Qty = 5;
SELECT * FROM dbo.WarehouseStock;
GO
Output
| Qty |
|---|
| 50 |
Output
| Qty |
|---|
| 500 |
Output
| ItemID | ItemName | Qty |
|---|---|---|
| 1 | Laptop | 55 |
| 2 | Mouse | 495 |
| 3 | Keyboard | 300 |
| 4 | Monitor | 80 |
Other techniques
- Keep transactions short; never wait for user input inside a transaction.
- Add indexes so UPDATE/DELETE touch fewer rows (scans lock more rows).
- Use READ COMMITTED SNAPSHOT so readers don't take shared locks.
- SET DEADLOCK_PRIORITY LOW for less important jobs (they become the victim).
- SET LOCK_TIMEOUT 5000 so a blocked statement fails after 5 seconds instead of waiting forever.
SET LOCK_TIMEOUT 5000; -- milliseconds, -1 = wait forever (default)
SELECT @@LOCK_TIMEOUT AS LockTimeoutMs;
SET LOCK_TIMEOUT -1;
GO
Output
| LockTimeoutMs |
|---|
| 5000 |
Practice Questions
- Create a deadlock between dbo.WarehouseStock and dbo.CustomerWallet (two different tables).
- Repeat it with SET DEADLOCK_PRIORITY LOW in window 2. Which window is the victim now?
- While blocked, run C1 - C3 and write down: blocked session, blocking session, lock modes.
- Use SET LOCK_TIMEOUT 3000 in window 2 and read the error number you get.