powershelldba.de

Deadlocks: Questions & Answers

Deadlocks are SQL Server's most mysterious errors. Two queries lock each other, one gets killed, and the application retries. But why? And how do you find the root cause when the error log shows nothing useful?

What Is a Deadlock?

Q: What exactly is a deadlock? Two or more transactions hold locks that each other needs, creating a circular wait. SQL Server detects this and kills one transaction (the "victim"), rolling it back.

Visual Example

Transaction A:              Transaction B:
1. LOCK Table1             1. LOCK Table2
2. Try to LOCK Table2 ←→ 2. Try to LOCK Table1
   (WAITS)                    (WAITS)

Result: CIRCULAR DEPENDENCY
SQL Server detects → Kills one (victim)
Victim rolls back → Other continues

Deadlock Myths & Facts

Q: Does increasing locks cause more deadlocks? No. Deadlocks are caused by conflicting access patterns, not lock quantity. More locks can actually reduce deadlock risk by being more granular.
Q: Does a deadlock corrupt data? No. Deadlocks are safe—one transaction is rolled back completely, no partial writes. It's an error, not a data integrity issue.
Q: Can I prevent deadlocks entirely? No. Deadlocks are inherent to multi-user systems with row-level locking. You minimize them through design, but can't eliminate them.
Q: Should I set DEADLOCK_PRIORITY to protect critical transactions? Yes, sparingly. Set higher priority for critical work (AP layer, batch jobs) so less important transactions get killed instead.

Root Causes

Q: What causes deadlocks? Access order mismatch:

Scenario A (Classic):
• Proc1: UPDATE Table1 → UPDATE Table2
• Proc2: UPDATE Table2 → UPDATE Table1
→ Deadlock if both run concurrently

Scenario B (Foreign Keys):
• Parent-child update cascades lock parent first, then child
• Insert order determines lock sequence
→ Deadlock if insert order varies

Scenario C (Range Locks):
• SELECT with WHERE clause locks a range
• INSERT into that range waits
• UPDATE of locked range creates circular wait

Diagnosing Deadlocks

Method 1: SQL Server Error Log

-- Look for deadlock entries in error log
-- But this shows minimal info (only victim SPID)

-- Enable trace flag 1222 (detailed deadlock graph)
DBCC TRACEON (1222, -1);  -- -1 = global

-- Now deadlocks are logged with full graph showing:
-- - Which transactions blocked which
-- - What statements they were running
-- - Lock modes held and waited-for

Method 2: Extended Events (SQL 2012+, Recommended)

-- Create session to capture deadlock graphs
CREATE EVENT SESSION deadlock_tracking ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file(
  SET filename = 'C:\SQLLogs\deadlocks.xel'
)
WITH (STARTUP_STATE = ON);

-- Parse the deadlock graph (shows victim, blocking chain, SQL statements)
-- Use Extended Events viewer in SSMS: Management → Extended Events → Sessions
🔧 DeadlockCollector Tool: Automate deadlock collection and analysis. Our DeadlockCollector tool continuously monitors for deadlock events, stores them in a structured database, and provides historical trending and pattern analysis—making identification of systemic deadlock issues dramatically easier than manual log parsing.

Method 3: Deadlock Graph Interpretation

-- A typical deadlock graph shows:
--
-- Process A (SPID 52)
--   Holding: Exclusive lock on Table1:Row 123
--   Waiting: Exclusive lock on Table2:Row 456
--   Running: UPDATE Table1 SET ... ; UPDATE Table2 SET ...
--
-- Process B (SPID 48)
--   Holding: Exclusive lock on Table2:Row 456
--   Waiting: Exclusive lock on Table1:Row 123
--   Running: UPDATE Table2 SET ... ; UPDATE Table1 SET ...
--
-- → Circular wait detected → SPID 52 killed as victim

-- Key insight: Both queries touch the same tables in different order!

Common Patterns & Solutions

Pattern 1: Order Mismatch Between Procedures

-- PROCEDURE A
CREATE PROCEDURE usp_UpdateCustomerAndOrder
  @CustomerID INT,
  @OrderID INT
AS
BEGIN
  UPDATE dbo.Customers SET LastModified = GETDATE() WHERE CustomerID = @CustomerID;
  UPDATE dbo.Orders SET LastModified = GETDATE() WHERE OrderID = @OrderID;
END;

-- PROCEDURE B
CREATE PROCEDURE usp_UpdateOrderAndCustomer
  @OrderID INT,
  @CustomerID INT
AS
BEGIN
  UPDATE dbo.Orders SET LastModified = GETDATE() WHERE OrderID = @OrderID;
  UPDATE dbo.Customers SET LastModified = GETDATE() WHERE CustomerID = @CustomerID;
END;

-- If both run concurrently → DEADLOCK
-- A locks Customers, waits for Orders
-- B locks Orders, waits for Customers

-- FIX: Standardize access order (always Customers first, then Orders)
ALTER PROCEDURE usp_UpdateOrderAndCustomer
  @OrderID INT,
  @CustomerID INT
AS
BEGIN
  UPDATE dbo.Customers SET LastModified = GETDATE() WHERE CustomerID = @CustomerID;
  UPDATE dbo.Orders SET LastModified = GETDATE() WHERE OrderID = @OrderID;
END;

Pattern 2: Foreign Key Cascade Locks

-- Schema:
-- Customers (PK: CustomerID) ← Orders.CustomerID (FK)

-- Insert order matters
-- Process A:
INSERT INTO Orders (OrderID, CustomerID) VALUES (1, 100);  -- Locks Customers:100

-- Process B (concurrent):
INSERT INTO Orders (OrderID, CustomerID) VALUES (2, 100);  -- Waits for Customers:100

-- Then both try to update Customers
-- → Potential deadlock with other processes

-- FIX: Use SERIALIZABLE or indexed views for FK integrity
-- OR: Accept deadlock as normal and retry in application layer

Pattern 3: Implicit Conversions in WHERE Clauses

-- Table definition:
CREATE TABLE Orders (
  OrderID INT PRIMARY KEY,
  OrderDate DATETIME,
  Amount DECIMAL(10,2)
);

-- Query with implicit conversion
SELECT * FROM Orders WHERE OrderID = '123';  -- String '123' implicitly converts to INT

-- This causes SQL Server to:
-- 1. Lock more rows than expected (type conversion might affect index scans)
-- 2. Range locks instead of single row
-- 3. Higher deadlock probability

-- FIX: Use correct data types
SELECT * FROM Orders WHERE OrderID = 123;  -- INT literal, not string

Prevention Strategies

Q: How do I prevent deadlocks in application code?

1. Consistent Access Order Always lock resources in the same order. If Proc A needs Table1 then Table2, ensure Proc B also locks Table1 first.

2. Minimize Lock Duration Keep transactions small and fast. Remove expensive operations (network calls, file I/O) from transactions.

3. Use Appropriate Isolation Level READ COMMITTED (default) is fine for most apps. SERIALIZABLE is safer but slower. SNAPSHOT can reduce deadlocks.

4. Retry Logic in Application Catch deadlock (error 1205) and retry with exponential backoff.

5. Index Optimization Better indexes = faster row access = shorter lock hold times = fewer deadlocks.

Retry Logic Example (C#)

int maxRetries = 3;
int retryCount = 0;
int backoffMs = 100;

while (retryCount < maxRetries)
{
  try
  {
    using (SqlConnection conn = new SqlConnection(connectionString))
    {
      conn.Open();
      using (SqlCommand cmd = new SqlCommand("usp_UpdateCustomer", conn))
      {
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.ExecuteNonQuery();
      }
    }
    break;  // Success
  }
  catch (SqlException ex) when (ex.Number == 1205)  // Deadlock
  {
    retryCount++;
    if (retryCount >= maxRetries) throw;
    System.Threading.Thread.Sleep(backoffMs);
    backoffMs *= 2;  // Exponential backoff
  }
}

Monitoring & Tools

🔧 DeadlockCollector (Recommended):

This PowerShell-based tool:
• Monitors SQL Server for deadlock events
• Captures and stores deadlock graphs in a structured database
• Provides queries to analyze deadlock patterns over time
• Identifies frequently deadlocked tables/procedures
• Enables historical trending and alerting

Visit the DeadlockCollector documentation for setup and configuration instructions.

Manual Deadlock Monitoring Query

-- Count deadlocks by executing:
SELECT
  OBJECT_NAME(i.object_id) AS table_name,
  i.name AS index_name,
  s.user_updates,
  s.user_seeks,
  s.user_scans
FROM sys.dm_db_index_usage_stats s
INNER JOIN sys.indexes i
  ON s.object_id = i.object_id
  AND s.index_id = i.index_id
WHERE database_id = DB_ID()
ORDER BY s.user_updates DESC;

-- This doesn't show deadlocks directly, but high update counts
-- on specific tables indicate deadlock hotspots

When to Accept Deadlocks

Q: Are occasional deadlocks acceptable? Yes. A few deadlocks per day (or week, depending on volume) are normal in busy systems. The key is:

• Application handles retries gracefully
• No data corruption occurs
• Users don't experience visible outages
• Trends are stable (not growing)

If deadlocks spike or are happening every minute, investigate root cause. Otherwise, focus on other performance issues.

Common Mistakes

Mistake 1: Using NOLOCK to "prevent" deadlocks
NOLOCK doesn't prevent deadlocks; it just causes dirty reads. Avoid.
Mistake 2: Setting TRANSACTION ISOLATION LEVEL SERIALIZABLE "for safety"
SERIALIZABLE prevents deadlocks in some cases but tanks performance and can itself cause different deadlocks.
Mistake 3: Ignoring deadlock errors in application code
Applications must catch error 1205 and retry. If you ignore it, users see errors you could have prevented.
Mistake 4: Enabling trace flag 1222 globally without log rotation
Deadlock graphs are verbose. They'll fill your error log quickly. Use Extended Events instead.

Deadlock Checklist

The Bottom Line

Deadlocks are not bugs—they're a feature of multi-user database systems. The question isn't "how do I eliminate them," but "how do I minimize their impact?"

Consistent access order, minimal transaction duration, good indexing, and application-level retry logic are your arsenal. Pair these with tools like DeadlockCollector to identify systemic issues, and you'll have a robust, resilient database layer.

And remember: occasional deadlocks are normal. Constant deadlocks are a symptom. Find the root cause, not just the error.

← Back to Blog