skip to content

SERIALIZABLE can be implemented by strict two-phase locking with predicate or next-key locks, or by Serializable Snapshot Isolation (SSI). Contrast how each achieves serial-equivalence, including how each stops a transaction from missing rows another transaction inserts, and what each costs at runtime.

level: seniorimportance: should knowfreq 46%

answer

  1. pessimistic prevents vs optimistic detects
  2. strict 2PL: locks to commit + gap/next-key for phantoms
  3. SSI: snapshot + rw anti-dependency pivot -> abort
  4. 2PL fails as deadlock; SSI as false-positive abort
  5. narrow read sets + short transactions help both

basics

~20 s

Locking prevents non-serializable schedules up front: locks held to commit, plus predicate or index-range locks so nobody can insert into a range you read — cost is blocking and deadlocks. SSI runs on snapshots, tracks read-write dependencies, and aborts a transaction when a dangerous pattern appears — cost is false-positive aborts.

solid answer

~60 s

**Strict 2PL (pessimistic).** Every read takes a shared lock, every write an exclusive lock, and all locks are held to commit. Conflicting transactions block, so the schedule is serial-equivalent by construction. Phantoms need extra machinery: since you cannot lock a row that does not exist, the engine locks the *predicate* or, more commonly, the index range — gap and next-key locks over the keys your scan touched — so an insert into that range must wait. Costs: readers block writers, lock footprints grow with scan size (an unindexed predicate can lock nearly everything scanned), and deadlocks are the failure mode. **SSI (optimistic).** Transactions run on snapshots with no read locks. The engine records what each transaction read, using lightweight non-blocking read markers that also cover ranges, and watches for read-write dependencies. When a pattern that cannot correspond to any serial order appears, it aborts one participant with a serialization failure. Costs: tracking memory, granularity escalation that produces false positives, and abort rates that climb with contention and transaction length. Same guarantee; you tune waits in one and aborts in the other.

code

text · 8 lines
text
T1: SELECT sum(amount) FROM tx WHERE account = 7;   -- range read

2PL  : S-locks matching index entries + the gaps around them (next-key locks)
       T2's INSERT INTO tx(account) VALUES (7) WAITS until T1 commits

SSI  : no locks; a read marker is recorded over the scanned range
       T2's INSERT proceeds; an rw-dependency edge T1 -> T2 is recorded
       if the edges form the dangerous pivot pattern, one transaction is ABORTED (40001)

go deeper

for a junior

Know that one family blocks conflicting transactions and the other lets them run and aborts on conflict.

for a middle

Explain why row locks are insufficient for phantoms and what range or predicate locking adds; name abort versus wait as the failure modes.

for a senior

Discuss the anti-dependency pivot idea, read-set granularity and escalation, false positives, and how indexing and transaction length tune each mechanism.

for a principal

Choose per workload — read-heavy and long versus short and hot — and set policy on transaction length, read-set size and which mechanism the platform standardises on.

## The two philosophies Serial-equivalence can be achieved by **preventing** bad interleavings before they happen (pessimistic locking) or by **detecting** them and aborting (optimistic validation). Both are correct; they fail differently. ## Strict two-phase locking Two-phase locking means each transaction has a growing phase (acquire locks) and a shrinking phase (release). *Strict* 2PL releases nothing until commit or rollback, which also guarantees recoverability. Shared locks conflict with exclusive locks, so any two transactions whose access sets overlap in a conflicting way are ordered by the lock manager — and the resulting schedule is provably equivalent to a serial one. **The phantom problem.** Row locks are not enough. A transaction that evaluates `WHERE status = 'NEW'` has locked the rows that matched, but a concurrent INSERT of a new matching row conflicts with nothing, and the outcome may match no serial order. Two solutions: - **Predicate locks** — lock the condition itself, so any write whose row satisfies the predicate conflicts. Conceptually clean, expensive to evaluate for arbitrary predicates, so rarely implemented in full. - **Index-range locking (gap / next-key locks)** — the practical approximation. The engine locks index entries *and the gaps between them* along the range your scan traversed, so an insert whose key falls in a locked gap must wait. Precision follows the index: a well-indexed equality predicate locks a narrow range; a predicate with no usable index degenerates into locking everything scanned. **Runtime costs.** Reads block writes and vice versa, so throughput falls as conflicting access grows. Lock-manager memory grows with the number of locks, sometimes triggering escalation to coarser granularity, which collapses concurrency further. Deadlocks are inevitable when transactions acquire locks in different orders; the engine detects them (wait-for graph or timeout) and aborts a victim — so even the pessimistic path needs retry logic. Latency becomes bimodal: fast when uncontended, blocked for the duration of a competing transaction when not. ## Serializable Snapshot Isolation SSI starts from snapshot isolation — every transaction reads a consistent point-in-time view, readers never block writers — and adds *just enough* bookkeeping to catch the schedules snapshot isolation would get wrong. The theory: dependencies between transactions form a graph. Write-write and write-read dependencies are already prevented or ordered by snapshot rules. The dangerous case is the **read-write anti-dependency**: T1 reads something that T2 then writes, so T1 must come before T2 in any equivalent serial order. Non-serializable schedules under snapshot isolation always contain a specific structure: a transaction with an incoming *and* an outgoing anti-dependency (a pivot), with a particular commit ordering. SSI tracks those edges and, when the dangerous structure appears, aborts one of the transactions involved. **How reads are tracked.** Instead of blocking locks, the engine records non-blocking read markers on the tuples, index pages or ranges a transaction examined — these behave like predicate locks for detection purposes only. A later writer touching a marked item creates a tracked edge. Because markers can be escalated to coarser granularity (page, relation) under memory pressure, detection is conservative. **Runtime costs.** No read blocking — the big win, especially for long or read-heavy transactions. But: tracking structures consume memory and must be retained until transactions can no longer participate in a cycle; escalation and the conservative test cause **false-positive aborts**, transactions killed although their schedule was actually fine; abort rates grow with contention, with the size of read sets, and with transaction duration; and even read-only transactions can be aborted as part of a dangerous structure. The guarantee also holds only if *all* participating transactions run at SERIALIZABLE — a concurrent transaction at a weaker level is not tracked and can create anomalies the mechanism cannot see. ## Choosing and tuning - **Read-heavy or long transactions:** SSI is strongly preferable — no read blocking. Under 2PL a long read transaction stalls the write workload. - **Short, hot, highly contended writes:** locking often wins, because a wait costs less than repeatedly redoing work that will lose again. - **Tuning 2PL:** index the predicates so range locks stay narrow, order access consistently, keep transactions short, watch deadlock rate and lock waits. - **Tuning SSI:** index the predicates so read sets and markers stay small, keep transactions short to reduce edge lifetime, size the tracking memory to avoid escalation, watch the abort rate. Note the common thread: **narrow read sets and short transactions help both**, because both mechanisms key off what a transaction read. ## The interview-level summary 2PL buys serial-equivalence with waiting and pays in deadlocks and blocked readers; SSI buys it with dependency tracking and pays in false-positive aborts and memory. Both require retry logic in the application — for a deadlock victim in one case, a serialization failure in the other.

  • Why can SSI abort a transaction whose schedule was actually serializable?
    Detection is conservative and approximate. The engine aborts on a structural pattern of read-write dependencies that is necessary but not sufficient for a real cycle, and it tracks reads at whatever granularity it can afford — escalating from tuple to page or relation under memory pressure, which invents dependencies that do not exist at row level. Both effects trade some false positives for cheap, non-blocking detection.
  • Does SSI still give the guarantee if some concurrent transactions run at a weaker isolation level?
    No. The mechanism only tracks dependencies among transactions running at SERIALIZABLE; a concurrent READ COMMITTED transaction is invisible to the analysis and can create exactly the anomaly the level was supposed to exclude. Enforcing serializability therefore means enforcing it for every transaction that touches the data involved in the invariant, not only the one you care about.

Locking is a librarian who will not let two people into the same aisle; SSI lets everyone browse freely and, at checkout, cancels an order once it can prove the browsing histories could not have happened one visitor at a time.

saying these in an interview costs you the question

  • Claiming row locks alone make a locking implementation serializable — range or predicate locking is required for phantoms.
  • Describing SSI as just snapshot isolation, ignoring the read-write dependency tracking that makes it serializable.
  • Saying SSI never aborts transactions that were actually fine — false positives are inherent to its conservative detection.
  • Forgetting that a locking implementation still needs application retry logic because deadlock victims are aborted.
  • Assuming SSI's guarantee holds when other concurrent transactions run at a weaker level.

context