Deadlocks, how SQL Server breaks them, and how to stop causing them
What a deadlock is, how the victim is chosen, the three shapes that cause most of them, and the retry every caller needs anyway.
After this lesson you can
- Explain a deadlock as a cycle in the wait-for graph and how SQL Server resolves it
- Recognise access-order, lock-conversion and key-lookup deadlocks and their fixes
- Write a bounded retry for error 1205
Blocking is one session waiting for a lock another holds; it ends when the holder finishes. A deadlock is a cycle of waits: session A holds a lock B needs, while waiting for a lock B holds. Neither can ever finish, so the engine has to break the cycle.
How SQL Server resolves it
A background lock monitor searches the wait-for graph for cycles, by default every 5 seconds and more often once it starts finding them. When it finds one it picks a victim, rolls back the victim's transaction, and raises error 1205 in the victim's session. The other sessions continue.
The victim is the session with the lowest DEADLOCK_PRIORITY
(LOW, NORMAL, HIGH, or -10 to 10; default NORMAL). Among equal
priorities, SQL Server picks the one it estimates is cheapest to roll
back.
Deadlock graphs are captured by default in the system_health extended
events session as xml_deadlock_report events (it keeps a limited
history, so old ones roll off). That graph names each process, the
statement it ran, the lock it held, and the lock it waited for. It is
the first thing to read.
The sandbox runs one session at a time, so it cannot produce a real
deadlock. The examples below model one instead: the first treats a
snapshot of who-waits-for-whom as a table, the way you would read
blocking_session_id from sys.dm_exec_requests.
CREATE TABLE waits (waiter INT PRIMARY KEY, holder INT NOT NULL, resource NVARCHAR(40) NOT NULL);INSERT INTO waits VALUES (51, 52, N'KEY: orders id=10'), (52, 51, N'KEY: customers id=7'), (60, 51, N'KEY: orders id=10'), (61, 62, N'OBJECT: stock');SELECT w.waiter, w.holder, w.resource, CASE WHEN EXISTS (SELECT 1 FROM waits AS x WHERE x.waiter = w.holder) THEN N'holder is waiting too' ELSE N'holder is running' END AS holder_stateFROM waits AS wORDER BY w.waiter;Three common shapes
- Opposite access order. One procedure updates
ordersthencustomers; another updatescustomersthenorders. Fix: touch tables (and rows) in one agreed order everywhere. - Lock conversion. Two sessions read the same row under
REPEATABLE READorSERIALIZABLE, both keep their shared lock, and both then try to update it. Each waits for the other's shared lock to go. Fix: read withWITH (UPDLOCK)when you intend to update, so the second reader waits at the read instead. - Key lookup against update. A reader seeks a nonclustered index and then looks up the clustered index; a writer updates the clustered row and then the nonclustered index entry. They take the two locks in opposite order. Fix: a covering index removes the lookup.
General remedies: keep transactions short, index so statements touch few rows, and consider RCSI, under which plain reads take no shared locks and so stop taking part in reader-writer deadlocks.
Retry, because deadlocks still happen
A deadlock victim's transaction was rolled back, so it is safe to run the whole transaction again. Retry on 1205 a small number of times with a short delay, and only for errors that a retry can fix.
The example below does not contain a deadlock. It simulates one:
THROW cannot raise error numbers below 50000, so the procedure raises
a custom error 51205 on its first two attempts to stand in for the real
1205, and the caller retries on either number. What it shows is the
retry loop and the rollback of each failed attempt, not how SQL Server
picks a victim.
CREATE TABLE accounts (id INT PRIMARY KEY, balance INT NOT NULL);CREATE TABLE attempts (n INT NOT NULL);INSERT INTO accounts VALUES (1, 100), (2, 0);INSERT INTO attempts VALUES (0);GOCREATE PROCEDURE dbo.transfer @amount INTASBEGIN SET NOCOUNT ON; SET XACT_ABORT ON; BEGIN TRY BEGIN TRAN; UPDATE accounts SET balance = balance - @amount WHERE id = 1; UPDATE attempts SET n = n + 1; IF (SELECT n FROM attempts) <= 2 THROW 51205, N'Simulated: chosen as deadlock victim.', 1; UPDATE accounts SET balance = balance + @amount WHERE id = 2; COMMIT; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK; UPDATE attempts SET n = n + 1; -- keep the count outside the rolled-back work THROW; END CATCH;END;DECLARE @log TABLE (attempt INT, outcome NVARCHAR(40));DECLARE @attempt INT = 1, @done BIT = 0; WHILE @done = 0 AND @attempt <= 5BEGIN BEGIN TRY EXEC dbo.transfer @amount = 30; INSERT INTO @log VALUES (@attempt, N'committed'); SET @done = 1; END TRY BEGIN CATCH INSERT INTO @log VALUES (@attempt, CONCAT(N'error ', ERROR_NUMBER())); IF ERROR_NUMBER() NOT IN (1205, 51205) BREAK; WAITFOR DELAY '00:00:00.050'; SET @attempt += 1; END CATCH;END; SELECT l.attempt, l.outcome, (SELECT balance FROM accounts WHERE id = 1) AS balance_1, (SELECT balance FROM accounts WHERE id = 2) AS balance_2FROM @log AS lORDER BY l.attempt;Common mistakes
- Retrying a single statement instead of the whole transaction. The victim's earlier statements were rolled back too.
- Retrying forever, or on every error. Bound the attempts; a constraint violation will not go away on the next try.
- Fixing deadlocks with
NOLOCK. It hides reader-writer deadlocks by allowing dirty reads; writer-writer cycles remain. - Guessing instead of reading the deadlock graph from
system_health.
What interviewers ask
- Deadlock or blocking? Blocking resolves when the holder finishes; a deadlock is a cycle that only ends when a victim is killed.
- How does SQL Server choose the victim? Lowest
DEADLOCK_PRIORITY, then the cheapest transaction to roll back. - How would you investigate and fix a recurring deadlock? Read the
graph from
system_health, find the two statements and lock order, then fix access order, add a covering index orUPDLOCK, and add a 1205 retry.
Exercise · Pro
Which sessions are in a deadlock
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade