How do you handle deadlocks in SQL databases?
Answer
Deadlocks occur when two or more transactions are waiting for each other to release locks, creating a circular dependency that prevents any of the transactions from proceeding. Database systems automatically detect and resolve deadlocks by terminating one of the transactions (deadlock victim).
Understanding Deadlocks
Classic Deadlock Scenario
-- Transaction 1
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- Waits for lock on account_id = 2
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;
-- Transaction 2 (running simultaneously)
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE account_id = 2;
-- Waits for lock on account_id = 1 (DEADLOCK!)
UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
COMMIT;
Deadlock Detection
-- SQL Server: Monitor deadlocks
SELECT
session_id,
blocking_session_id,
wait_type,
wait_time,
wait_resource
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0;
-- Enable deadlock monitoring
DBCC TRACEON(1222, -1); -- Log deadlock information
-- PostgreSQL: Check for locks
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS blocking_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity
ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
JOIN pg_catalog.pg_stat_activity blocking_activity
ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.GRANTED;
Deadlock Prevention Strategies
1. Consistent Lock Ordering
-- BAD: Inconsistent ordering leads to deadlocks
-- Transaction A
UPDATE table1 SET col1 = 'value' WHERE id = 1;
UPDATE table2 SET col2 = 'value' WHERE id = 2;
-- Transaction B
UPDATE table2 SET col2 = 'value' WHERE id = 2; -- Different order!
UPDATE table1 SET col1 = 'value' WHERE id = 1;
-- GOOD: Consistent ordering prevents deadlocks
-- Both transactions access resources in same order
-- Transaction A & B
UPDATE table1 SET col1 = 'value' WHERE id = 1; -- Always access table1 first
UPDATE table2 SET col2 = 'value' WHERE id = 2; -- Then table2
-- Example: Money transfer with consistent ordering
CREATE OR REPLACE FUNCTION transfer_money(
from_account INT,
to_account INT,
amount DECIMAL(10,2)
) RETURNS VOID AS $$
DECLARE
first_account INT;
second_account INT;
BEGIN
-- Always lock accounts in ascending order of account_id
IF from_account < to_account THEN
first_account := from_account;
second_account := to_account;
ELSE
first_account := to_account;
second_account := from_account;
END IF;
-- Lock in consistent order
UPDATE accounts SET balance = balance -
CASE WHEN account_id = from_account THEN amount ELSE -amount END
WHERE account_id IN (first_account, second_account)
ORDER BY account_id; -- Ensures consistent locking order
END;
$$ LANGUAGE plpgsql;
2. Minimize Transaction Scope
-- BAD: Long-running transaction holding locks
BEGIN TRANSACTION;
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 123;
-- ... lots of business logic, external API calls, user input ...
INSERT INTO orders (customer_id, product_id, quantity) VALUES (456, 123, 1);
-- ... more processing ...
COMMIT;
-- GOOD: Minimize lock duration
-- Prepare data first
DECLARE @customer_id INT = 456;
DECLARE @product_id INT = 123;
DECLARE @quantity INT = 1;
-- Quick transaction
BEGIN TRANSACTION;
UPDATE inventory SET quantity = quantity - @quantity WHERE product_id = @product_id;
INSERT INTO orders (customer_id, product_id, quantity)
VALUES (@customer_id, @product_id, @quantity);
COMMIT;
3. Use Appropriate Isolation Levels
-- Use lower isolation levels when possible
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Or use snapshot isolation to avoid blocking
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
-- PostgreSQL: Use advisory locks for application-level coordination
SELECT pg_advisory_lock(12345); -- Application-specific lock
-- ... perform operations ...
SELECT pg_advisory_unlock(12345);
-- SQL Server: Use application locks
EXEC sp_getapplock @Resource = 'MyResource', @LockMode = 'Exclusive';
-- ... perform operations ...
EXEC sp_releaseapplock @Resource = 'MyResource';
4. Lock Timeout Settings
-- SQL Server: Set lock timeout
SET LOCK_TIMEOUT 5000; -- 5 seconds
-- PostgreSQL: Set statement timeout
SET statement_timeout = '30s';
-- MySQL: Set lock wait timeout
SET innodb_lock_wait_timeout = 10;
-- Example with timeout handling
BEGIN TRY
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK;
IF ERROR_NUMBER() = 1222 -- Deadlock
BEGIN
PRINT 'Deadlock detected, retrying...';
-- Implement retry logic
END
ELSE
BEGIN
THROW; -- Re-throw other errors
END
END CATCH;
Deadlock Resolution Patterns
1. Retry Logic with Exponential Backoff
-- SQL Server stored procedure with retry logic
CREATE PROCEDURE TransferMoneyWithRetry
@FromAccount INT,
@ToAccount INT,
@Amount DECIMAL(10,2),
@MaxRetries INT = 3
AS
BEGIN
DECLARE @RetryCount INT = 0;
DECLARE @Success BIT = 0;
WHILE @RetryCount < @MaxRetries AND @Success = 0
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
UPDATE accounts
SET balance = balance - @Amount
WHERE account_id = @FromAccount AND balance >= @Amount;
IF @@ROWCOUNT = 0
THROW 50001, 'Insufficient funds', 1;
UPDATE accounts
SET balance = balance + @Amount
WHERE account_id = @ToAccount;
COMMIT;
SET @Success = 1;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK;
IF ERROR_NUMBER() = 1205 -- Deadlock victim
BEGIN
SET @RetryCount = @RetryCount + 1;
-- Exponential backoff: wait longer each retry
WAITFOR DELAY '00:00:01'; -- Base delay
WAITFOR DELAY DATEADD(ms, POWER(2, @RetryCount) * 100, 0);
END
ELSE
BEGIN
THROW; -- Re-throw non-deadlock errors
END
END CATCH
END
IF @Success = 0
THROW 50002, 'Transaction failed after maximum retries', 1;
END;
2. Application-Level Queueing
-- Create a queue table for serializing conflicting operations
CREATE TABLE transaction_queue (
queue_id SERIAL PRIMARY KEY,
account_id INT NOT NULL,
operation_type VARCHAR(20) NOT NULL,
amount DECIMAL(10,2),
target_account_id INT,
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP
);
-- Queue processor function
CREATE OR REPLACE FUNCTION process_transaction_queue()
RETURNS VOID AS $$
DECLARE
queue_item RECORD;
BEGIN
-- Process queue items in order
FOR queue_item IN
SELECT * FROM transaction_queue
WHERE status = 'PENDING'
ORDER BY created_at
FOR UPDATE SKIP LOCKED -- Skip locked rows to avoid blocking
LOOP
BEGIN
-- Process the transaction
IF queue_item.operation_type = 'TRANSFER' THEN
PERFORM transfer_money(
queue_item.account_id,
queue_item.target_account_id,
queue_item.amount
);
END IF;
-- Mark as processed
UPDATE transaction_queue
SET status = 'COMPLETED', processed_at = CURRENT_TIMESTAMP
WHERE queue_id = queue_item.queue_id;
EXCEPTION WHEN OTHERS THEN
-- Mark as failed
UPDATE transaction_queue
SET status = 'FAILED', processed_at = CURRENT_TIMESTAMP
WHERE queue_id = queue_item.queue_id;
END;
END LOOP;
END;
$$ LANGUAGE plpgsql;
3. Optimistic Concurrency Control
-- Add version column for optimistic locking
ALTER TABLE accounts ADD COLUMN version_number INT DEFAULT 1;
-- Update with version check
CREATE OR REPLACE FUNCTION update_account_balance(
p_account_id INT,
p_amount DECIMAL(10,2),
p_expected_version INT
) RETURNS BOOLEAN AS $$
DECLARE
rows_affected INT;
BEGIN
UPDATE accounts
SET balance = balance + p_amount,
version_number = version_number + 1
WHERE account_id = p_account_id
AND version_number = p_expected_version;
GET DIAGNOSTICS rows_affected = ROW_COUNT;
RETURN rows_affected > 0; -- Returns false if version mismatch
END;
$$ LANGUAGE plpgsql;
-- Usage with retry on version conflict
DO $$
DECLARE
current_version INT;
current_balance DECIMAL(10,2);
success BOOLEAN := FALSE;
retry_count INT := 0;
BEGIN
WHILE NOT success AND retry_count < 5 LOOP
-- Read current state
SELECT balance, version_number
INTO current_balance, current_version
FROM accounts
WHERE account_id = 123;
-- Check business rules
IF current_balance >= 100 THEN
-- Attempt update
success := update_account_balance(123, -100, current_version);
IF NOT success THEN
retry_count := retry_count + 1;
PERFORM pg_sleep(0.1 * retry_count); -- Brief delay
END IF;
ELSE
RAISE EXCEPTION 'Insufficient funds';
END IF;
END LOOP;
IF NOT success THEN
RAISE EXCEPTION 'Failed to update after % retries', retry_count;
END IF;
END $$;
Advanced Deadlock Handling
1. Deadlock Priority
-- SQL Server: Set deadlock priority
SET DEADLOCK_PRIORITY LOW; -- This session more likely to be victim
SET DEADLOCK_PRIORITY HIGH; -- This session less likely to be victim
SET DEADLOCK_PRIORITY NORMAL; -- Default priority
-- Use in stored procedures for background processes
CREATE PROCEDURE BackgroundDataCleanup
AS
BEGIN
SET DEADLOCK_PRIORITY LOW; -- OK to be deadlock victim
-- Cleanup operations that can be retried
DELETE FROM temp_data WHERE created_date < DATEADD(day, -7, GETDATE());
END;
2. Lock Hints and Query Optimization
-- Use appropriate lock hints to reduce deadlock potential
-- NOLOCK: Read uncommitted data (use carefully)
SELECT * FROM orders WITH (NOLOCK) WHERE customer_id = 123;
-- READPAST: Skip locked rows
SELECT * FROM queue_table WITH (READPAST) WHERE status = 'PENDING';
-- UPDLOCK: Acquire update locks early
SELECT account_id, balance
FROM accounts WITH (UPDLOCK)
WHERE account_id IN (1, 2);
-- Optimize queries to use indexes and reduce lock duration
-- BAD: Table scan holds many locks
UPDATE orders SET status = 'SHIPPED' WHERE order_date = '2024-01-15';
-- GOOD: Index seek holds fewer locks
UPDATE orders SET status = 'SHIPPED' WHERE order_id = 12345;
3. Partitioning to Reduce Lock Contention
-- Partition hot tables to reduce lock contention
CREATE TABLE orders (
order_id BIGSERIAL,
customer_id INT,
order_date DATE,
status VARCHAR(20),
total DECIMAL(10,2)
) PARTITION BY RANGE (order_date);
-- Create monthly partitions
CREATE TABLE orders_2024_01 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
-- Operations on different partitions won't deadlock each other
Monitoring and Alerting
1. Deadlock Monitoring Queries
-- SQL Server: Query deadlock information from system health session
WITH DeadlockData AS (
SELECT
CAST(event_data AS XML) AS event_data
FROM sys.fn_xe_file_target_read_file(
'system_health*.xel', NULL, NULL, NULL
)
WHERE object_name = 'xml_deadlock_report'
)
SELECT
event_data.value('(/event/@timestamp)[1]', 'datetime2') AS deadlock_time,
event_data.value('(/event/data/value)[1]', 'varchar(max)') AS deadlock_graph
FROM DeadlockData;
-- PostgreSQL: Create deadlock monitoring function
CREATE OR REPLACE FUNCTION monitor_locks()
RETURNS TABLE (
blocked_pid INT,
blocked_user TEXT,
blocking_pid INT,
blocking_user TEXT,
blocked_query TEXT,
blocking_query TEXT,
wait_time INTERVAL
) AS $$
BEGIN
RETURN QUERY
SELECT
bl.pid AS blocked_pid,
ba.usename AS blocked_user,
kl.pid AS blocking_pid,
ka.usename AS blocking_user,
ba.query AS blocked_query,
ka.query AS blocking_query,
CURRENT_TIMESTAMP - ba.query_start AS wait_time
FROM pg_locks bl
JOIN pg_stat_activity ba ON bl.pid = ba.pid
JOIN pg_locks kl ON bl.locktype = kl.locktype
AND bl.database IS NOT DISTINCT FROM kl.database
AND bl.relation IS NOT DISTINCT FROM kl.relation
AND bl.page IS NOT DISTINCT FROM kl.page
AND bl.tuple IS NOT DISTINCT FROM kl.tuple
AND bl.virtualxid IS NOT DISTINCT FROM kl.virtualxid
AND bl.transactionid IS NOT DISTINCT FROM kl.transactionid
AND bl.classid IS NOT DISTINCT FROM kl.classid
AND bl.objid IS NOT DISTINCT FROM kl.objid
AND bl.objsubid IS NOT DISTINCT FROM kl.objsubid
AND bl.pid != kl.pid
JOIN pg_stat_activity ka ON kl.pid = ka.pid
WHERE NOT bl.granted AND kl.granted;
END;
$$ LANGUAGE plpgsql;
2. Automated Deadlock Resolution
-- Create deadlock alert system
CREATE TABLE deadlock_alerts (
alert_id SERIAL PRIMARY KEY,
deadlock_time TIMESTAMP,
victim_spid INT,
deadlock_graph TEXT,
alert_sent BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Stored procedure to handle deadlock alerts
CREATE OR REPLACE FUNCTION handle_deadlock_alert(
p_victim_spid INT,
p_deadlock_info TEXT
) RETURNS VOID AS $$
BEGIN
-- Log the deadlock
INSERT INTO deadlock_alerts (deadlock_time, victim_spid, deadlock_graph)
VALUES (CURRENT_TIMESTAMP, p_victim_spid, p_deadlock_info);
-- Send alert if deadlocks are frequent
IF (SELECT COUNT(*) FROM deadlock_alerts
WHERE deadlock_time > CURRENT_TIMESTAMP - INTERVAL '1 hour') > 5 THEN
-- Trigger alert (could be email, Slack, etc.)
PERFORM pg_notify('deadlock_alert',
'High deadlock frequency detected: ' || p_deadlock_info);
END IF;
END;
$$ LANGUAGE plpgsql;
Best Practices Summary
1. Prevention Strategies
- Always access resources in consistent order
- Keep transactions short and focused
- Use appropriate isolation levels
- Implement proper indexing to reduce lock duration
- Consider partitioning for high-contention tables
2. Detection and Resolution
- Implement retry logic with exponential backoff
- Use deadlock priority appropriately
- Monitor deadlock frequency and patterns
- Set reasonable lock timeouts
3. Application Design
- Use optimistic concurrency control when appropriate
- Implement application-level queueing for conflicting operations
- Design for idempotency to handle retries safely
- Consider eventual consistency models where strict consistency isn’t required
Interview Tips
- Understand the root causes of deadlocks (circular wait conditions)
- Know prevention strategies: consistent ordering, short transactions, appropriate isolation levels
- Be familiar with database-specific deadlock detection and resolution mechanisms
- Understand retry patterns and when to use them
- Know how to monitor and troubleshoot deadlock issues
- Practice designing deadlock-free transaction patterns
- Be aware of the trade-offs between performance and deadlock prevention
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.