Deadlocks and row versioning (snapshot isolation)
When two sessions block each other for ever: how SQL Server finds deadlocks and picks a victim, Msg 1205, retrying, reading the deadlock graph, and how to prevent them. Then row versioning: READ COMMITTED SNAPSHOT, SNAPSHOT isolation, update conflicts and the version store.
Making a deadlock
BEGIN TRANSACTION;UPDATE dbo.Products SET Stock = Stock - 1 WHERE ProductId = 1; -- hammerBEGIN TRANSACTION;UPDATE dbo.Products SET Stock = Stock - 1 WHERE ProductId = 2; -- drillUPDATE dbo.Products SET Stock = Stock - 1 WHERE ProductId = 2; -- waitsUPDATE dbo.Products SET Stock = Stock - 1 WHERE ProductId = 1;Msg 1205, Level 13, State 51, Line 1 Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
SELECT @@TRANCOUNT AS OpenTransactions;COMMIT;OpenTransactions
----------------
1SELECT @@TRANCOUNT AS OpenTransactions;OpenTransactions
----------------
0SET DEADLOCK_PRIORITY LOW (or NORMAL, HIGH, or a number from -10 to 10): the session with the lower priority is chosen first. Give it to background jobs that can easily run again, so the till never loses a sale.
Retry the victim
SET DEADLOCK_PRIORITY LOW;SET XACT_ABORT ON;DECLARE @Attempt int = 1; WHILE 1 = 1BEGIN BEGIN TRY BEGIN TRANSACTION; UPDATE dbo.Products SET Stock = Stock - 1 WHERE ProductId = 2; UPDATE dbo.Products SET Stock = Stock - 1 WHERE ProductId = 1; COMMIT; PRINT CONCAT('Attempt ', @Attempt, ' committed'); BREAK; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK; IF ERROR_NUMBER() = 1205 AND @Attempt < 3 BEGIN PRINT CONCAT('Attempt ', @Attempt, ' was a deadlock victim; trying again'); SET @Attempt += 1; WAITFOR DELAY '00:00:00.100'; END ELSE THROW; END CATCH;END;Attempt 1 was a deadlock victim; trying again Attempt 2 committed
Finding out what deadlocked
WITH Reports AS ( SELECT CAST(event_data AS xml) AS Report FROM sys.fn_xe_file_target_read_file('system_health*.xel', NULL, NULL, NULL) WHERE object_name = 'xml_deadlock_report')SELECT TOP (1) Report.value('(event/@timestamp)[1]', 'datetime2(0)') AS HappenedAt, Report.value('(event/data/value/deadlock/victim-list/victimProcess/@id)[1]', 'varchar(20)') AS Victim, Report.value('count(event/data/value/deadlock/process-list/process)', 'int') AS ProcessesFROM ReportsORDER BY HappenedAt DESC;HappenedAt Victim Processes ------------------- ------------------ --------- 2026-11-09 20:08:04 process27a82403088 2
Report column instead and SSMS shows the XML; save it as a file ending in .xdl and SSMS draws the deadlock graph: the processes as ovals (the victim crossed out), the locked rows as boxes, and arrows for “holds” and “waits for”.
Preventing deadlocks
Touch rows and tables in the same order everywhere. If both sessions above had updated the hammer first, Session 2 would simply have waited for Session 1 to commit and then run; no cycle is possible. Keep transactions short. Fewer locks are held for less time. Never wait for user input inside a transaction. Do not read and then update under REPEATABLE READ or SERIALIZABLE. Two sessions holding S locks on the same row, both wanting X, deadlock every time. Read with UPDLOCK, or do it in one UPDATE. Index the columns you search on. An UPDATE that has to scan a whole table takes and checks far more locks than one that seeks to its rows. Use row versioning for readers (below): readers stop taking S locks, so reader–writer deadlocks disappear.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;BEGIN TRANSACTION;DECLARE @Stock int = (SELECT Stock FROM dbo.Products WHERE ProductId = 2);WAITFOR DELAY '00:00:02';UPDATE dbo.Products SET Stock = @Stock - 1 WHERE ProductId = 2;COMMIT;Msg 1205, Level 13, State 51, Line 5 Transaction (Process ID 55) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
Row versioning: readers that do not block
ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; SELECT is_read_committed_snapshot_onFROM sys.databasesWHERE database_id = DB_ID();Nonqualified transactions are being rolled back. Estimated rollback completion: 0%. Nonqualified transactions are being rolled back. Estimated rollback completion: 100%. is_read_committed_snapshot_on ----------------------------- 1
BEGIN TRANSACTION;UPDATE dbo.Products SET Price = 95.00 WHERE ProductId = 2;SELECT Name, Price FROM dbo.Products WHERE ProductId = 2;Name Price -------------- ----- Cordless drill 89.00
LCK_M_X).
SNAPSHOT isolation
ALTER DATABASE CURRENT SET ALLOW_SNAPSHOT_ISOLATION ON; SELECT snapshot_isolation_state_descFROM sys.databasesWHERE database_id = DB_ID();snapshot_isolation_state_desc ----------------------------- ON
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;BEGIN TRANSACTION;SELECT Name, Stock FROM dbo.Products WHERE ProductId = 2;Name Stock -------------- ----- Cordless drill 12
UPDATE dbo.Products SET Stock = Stock - 10 WHERE ProductId = 2;SELECT Name, Stock FROM dbo.Products WHERE ProductId = 2;Name Stock -------------- ----- Cordless drill 2
SELECT Name, Stock FROM dbo.Products WHERE ProductId = 2;UPDATE dbo.Products SET Stock = Stock - 1 WHERE ProductId = 2;Name Stock -------------- ----- Cordless drill 12 Msg 3960, Level 16, State 2, Line 2 Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot isolation to access table 'dbo.Products' directly or indirectly in database 'BuildMart' to update, delete, or insert the row that has been modified or deleted by another transaction. Retry the transaction or change the isolation level for the update/delete statement.
| READ COMMITTED SNAPSHOT | SNAPSHOT | |
|---|---|---|
SET READ_COMMITTED_SNAPSHOT ON (no other sessions connected) | SET ALLOW_SNAPSHOT_ISOLATION ON, then SET TRANSACTION ISOLATION LEVEL SNAPSHOT per session | |
Show a hint
Show the solution
CREATE OR ALTER PROCEDURE dbo.MoveStock @FromId int, @ToId int, @Qty intASBEGIN SET NOCOUNT ON; SET XACT_ABORT ON; UPDATE dbo.Products SET Stock = Stock + CASE WHEN ProductId = @ToId THEN @Qty ELSE -@Qty END WHERE ProductId IN (@FromId, @ToId);END;GOEXEC dbo.MoveStock @FromId = 3, @ToId = 1, @Qty = 5; SELECT ProductId, Name, Stock FROM dbo.Products WHERE ProductId IN (1, 3);ProductId Name Stock
--------- ---------------- -----
1 Claw hammer 45
3 Tape measure 8 m 60Remember
A deadlock is a cycle of sessions waiting for each other’s locks; it never ends by itself. The lock monitor finds it within seconds, rolls back a victim and raises error 1205; the other session carries on. DEADLOCK_PRIORITY chooses who is sacrificed; otherwise the cheapest to roll back is. Retry a victim’s whole transaction (catch 1205); it has been rolled back completely. system_health records every deadlock report; an .xdl file shows the graph in SSMS. Prevent deadlocks: same access order, short transactions, no read-then-write under REPEATABLE READ, good indexes. RCSI: READ COMMITTED readers read the last committed version per statement and are never blocked by writers. SNAPSHOT: one consistent view for the whole transaction; writing a row changed since then fails with 3960.