Locking — Shared, Exclusive and Update Locks

SQL Server uses locks to coordinate concurrent access to data. Understanding lock types, compatibility, and escalation is essential for diagnosing blocking — the most common performance complaint in SQL Server production systems. Most blocking stems from long-running transactions holding exclusive locks while other sessions wait for shared locks on the same rows. RCSI (from the previous lesson) eliminates most read/write blocking, but write/write blocking (two sessions updating the same rows) requires different strategies.

Locks and Blocking Diagnosis

-- ── Diagnose active blocking ───────────────────────────────────────────────
-- Who is blocked and who is blocking them:
SELECT
    r.session_id         AS BlockedSession,
    r.blocking_session_id AS BlockingSession,
    r.wait_type,
    r.wait_time / 1000.0 AS WaitSeconds,
    r.status,
    SUBSTRING(t.text, (r.statement_start_offset/2)+1,
        ((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(t.text)
          ELSE r.statement_end_offset END - r.statement_start_offset)/2)+1) AS BlockedSQL
FROM   sys.dm_exec_requests r
CROSS  APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE  r.blocking_session_id > 0;

-- ── What is the blocking session running? ────────────────────────────────
SELECT
    s.session_id,
    s.login_name,
    s.host_name,
    s.program_name,
    s.status,
    s.open_transaction_count,
    SUBSTRING(t.text, 1, 200) AS LastSQL
FROM   sys.dm_exec_sessions  s
CROSS  APPLY sys.dm_exec_sql_text(s.most_recent_sql_handle) t
WHERE  s.session_id = 55;   -- replace with the blocking session_id

-- ── Lock details for a session ────────────────────────────────────────────
SELECT
    tl.resource_type,
    tl.resource_description,
    tl.request_mode,      -- S=shared, X=exclusive, U=update, IS/IX=intent
    tl.request_status,    -- GRANT=has the lock, WAIT=waiting
    tl.request_session_id
FROM   sys.dm_tran_locks tl
WHERE  tl.resource_database_id = DB_ID('BlogApp')
ORDER  BY tl.request_session_id, tl.resource_type;

-- ── Lock compatibility matrix ─────────────────────────────────────────────
-- S  (Shared):    Compatible with S, IS     Incompatible with X, U, IX
-- X  (Exclusive): Incompatible with ALL     (only one writer at a time)
-- U  (Update):    Compatible with S, IS     Incompatible with U, X, IX
-- IS (Intent Shared):  Compatible with S, IS, IX, U
-- IX (Intent Exclusive): Compatible with IS, IX

-- ── WITH (NOLOCK) — READ UNCOMMITTED at query level ───────────────────────
-- ❌ Dangerous: reads uncommitted data and may miss rows or read duplicates
SELECT COUNT(*) FROM dbo.Posts WITH (NOLOCK);   -- approximate, not safe for logic

-- ── UPDLOCK hint — take update lock during read to prevent deadlocks ──────
-- Prevents two sessions from both taking shared locks, then both trying to upgrade
-- to exclusive (classic deadlock pattern):
BEGIN TRANSACTION;
    SELECT ViewCount FROM dbo.Posts WITH (UPDLOCK, ROWLOCK) WHERE Id = @PostId;
    -- UPDLOCK held — no other session can take UPDLOCK or XLOCK on this row
    UPDATE dbo.Posts SET ViewCount = ViewCount + 1 WHERE Id = @PostId;
COMMIT TRANSACTION;
Note: Lock escalation occurs when SQL Server acquires too many fine-grained locks (row or page locks) and promotes them to a single coarse-grained table lock. The default escalation threshold is 5,000 locks for a single statement. A single UPDATE that modifies 10,000 rows takes 10,000 row locks, which may escalate to a table lock — blocking all other access to the table for the duration of the update. To prevent escalation: break large updates into smaller batches, or use ALTER TABLE Posts SET (LOCK_ESCALATION = DISABLE) (use carefully — can increase memory usage).
Tip: Use the UPDLOCK hint when your pattern is “read a value, compute something, then write back.” Without UPDLOCK, two concurrent transactions both take shared locks (S), both read the same value, both try to upgrade to exclusive (X) — and deadlock. With UPDLOCK, the first transaction takes an update lock (U), the second waits (cannot take U while U is held), the first upgrades to X and writes, then the second proceeds. UPDLOCK prevents the escalation-to-deadlock pattern at the cost of serialising concurrent reads of that specific row.
Warning: Long-running transactions are the primary cause of blocking. A transaction that begins, executes a few statements, then waits for user input (or an external API call) holds its locks for the entire wait duration — potentially blocking other sessions for seconds or minutes. The golden rule: keep transactions as short as possible. Open the transaction, perform all database work, commit immediately. Never wait for external systems (HTTP calls, file I/O, user confirmation) while a transaction is open.

Common Mistakes

Mistake 1 — Transaction open during external API calls (long lock hold time)

❌ Wrong — BEGIN TRAN → call external payment API (200ms-5s) → commit; locks held during the entire external call.

✅ Correct — call external API first → open transaction → record result → commit immediately.

Mistake 2 — Read-then-update without UPDLOCK (deadlock under concurrency)

❌ Wrong — two sessions both SELECT (S lock), both try UPDATE (X lock) — deadlock.

✅ Correct — SELECT ... WITH (UPDLOCK) serialises access; first session reads and updates; second waits then proceeds.

🧠 Test Yourself

Two sessions simultaneously read a post with SELECT (shared lock) then try to UPDATE it (exclusive lock). Which locking problem occurs?