Transactions & Isolation · Deep dive · visual edition

Serializable Snapshot Isolation

~25 min · 5 levels · graph theory → runtime detector · interview questions + interactive recall

Published

Serializable snapshot isolationTransactionsIsolationACIDMVCC

Your bar: on a whiteboard, explain how a database can let everyone read a stale, non-blocking snapshot and still guarantee the result is equivalent to running transactions one at a time. The trick is a small piece of graph theory turned into a cheap, deliberately conservative runtime detector. Grounded in Fekete et al. (2005) 2 , Cahill et al. (2008) 1 , and the PostgreSQL implementation (Ports & Grittner, VLDB 2012) 3 .

One sentence holds the whole lesson. Each level unpacks one chip of it:

Snapshot isolation’s only hole is write skew

every anomaly is a cycle of rw-edges

all reduce to one dangerous structure

detect & abort, conservatively

THE WHOLE ARGUMENT — four moves SI is fast, not safe write skew anomaly = graph cycle rw-edges only one pivot, two rw-edges dangerous structure detect → abort SQLSTATE 40001 cheap because it never builds the full graph — it watches for one local shape conservative: catches every anomaly, plus a few innocent transactions (the price)
Memorise this banner. Levels 1–5 are exactly these four boxes plus the bill at the end.
1

Snapshot isolation’s blind spot — write skew

SI blocks dirty reads, non-repeatable reads, and lost updates — but not this.

Write skew breaks an invariant no single transaction violates. Two on-call doctors each read 2 doctors on call — fine, I can go off-call, then each updates their own row. Neither write conflicts with the other (different rows), so SI's first-committer-wins never fires. Both commit; zero doctors are on call.

Write skew breaks an invariant no single transaction violates. Two on-call doctors each read “2 on call — the other covers it,” then each updates their own row. Different rows → no write-write conflict → first-committer-wins never fires. Both commit; zero doctors on call. No serial order produces this.

Tx A (Alice) Tx B (Bob) SELECT count(*) → 2 ✓ SELECT count(*) → 2 ✓ UPDATE alice off-call UPDATE bob off-call COMMIT ✓ COMMIT ✓ result: 0 doctors on call — invariant broken
Both read the same condition on overlapping rows, then each writes a different row. SI sees no conflict.

Is Two txns read an overlapping set, each writes a different row whose safety depended on what the other read.

Why SI misses it Different rows → no write-write conflict → first-committer-wins (which only catches same-row clashes) never fires.

Pattern Any “check a condition, then act on the assumption it still holds.” Last-unit inventory, duplicate username, double-booked room.

Citation Berenson/Gray “Critique of ANSI SQL Isolation Levels” (1995); Oracle’s SERIALIZABLE is really only SI. 5

✗ “Snapshot isolation is serializable”

✓ SI permits write skew; only SSI / 2PL / serial execution are truly serializable

🩺 Memory rule: Write skew = read an overlapping condition, each write a different row — SI’s first-committer-wins never sees it.

Memory check
  • Why doesn't first-committer-wins catch write skew? → the two writes hit different rows
  • One real-world instance? → both claim the last inventory unit / username
  • Does a serial order ever produce zero doctors? → no; the 2nd would read count=1 and be refused
2

Serializability is a graph property

To detect non-serializability you first need to define it — and the definition is graph-shaped.

Three dependency edges and the rule. A wr edge (T1 writes x, T2 reads that version) and a ww edge (T1's version is overwritten by T2) both order T1 before T2. The rw-antidependency is the subtle one: T1 reads the old version and T2 writes a newer one, so T1 must have come first. A history is serializable if and only if this graph is acyclic.

Three dependency edges, and the rule. wr (write→read) and ww (write→write) order T1→T2 the obvious way. The rw -antidependency is subtle: T1 reads the old version, T2 writes a newer one — T1 didn’t see it, so T1 must precede T2. A history is serializable iff this graph is acyclic.

EdgeMeaningOrdersUnder SI?
wrT1 writes x; T2 reads that versionT1 → T2Never crosses concurrent txns — you read only committed snapshot versions
wwT1 writes x; T2 overwrites itT1 → T2Blocked — first-committer-wins stops two concurrent writers of x
rw

T1 reads x; T2 writes a later x

T1 → T2

The only edge SI can’t prevent — your snapshot is frozen, later writes are invisible yet still order you first

Master theorem A multiversion history is serializable exactly when its dependency graph has no cycle. (Adya 1999; Bernstein/Hadzilacos/Goodman.) 6

Why a cycle is fatal T1→T2→…→T1 asserts each txn is both before and after the others — impossible to lay on a line.

The narrowing SI already prevents ww and cross-txn wr cycles, so the only way an SI history breaks is a cycle made of rw-edges.

Why it matters The whole job of a serializable engine reduces to one rule: don’t let a cycle commit.

✗ “You must build and topologically sort the whole graph”

✓ You only need to prevent rw-cycles — and §3 shrinks even that

🔗 Memory rule: Serializable ⟺ acyclic dependency graph; under SI the only cycles left are made of rw-antidependencies.

Memory check
  • Serializable iff the graph is…? → acyclic
  • Which edge can SI not prevent? → rw (read→write antidependency)
  • Why can't SI produce a cross-txn wr cycle? → you only read committed snapshot versions
3

The dangerous structure — two rw-edges and a pivot

The result SSI is built on, and the reason it can be cheap. Sharper than “watch for cycles.”

Every snapshot-isolation anomaly contains this exact shape. In any non-serializable SI history, some transaction T-pivot has BOTH an incoming rw-edge (T-in to T-pivot) and an outgoing rw-edge (T-pivot to T-out) — two rw-antidependencies in a row, meeting at the pivot. Both conflicting transactions are concurrent with the pivot, and T-out commits first.

Every SI anomaly contains this exact shape. Some T-pivot has BOTH an inbound rw-edge (from T-in) and an outbound one (to T-out) — two rw-antidependencies in a row. Both conflicting txns are concurrent with the pivot; T-out commits first (Fekete’s refinement).

T-in reads x T-pivot in + out flag T-out commits first rw rw break ANY one of the three → the cycle can never close
You don't materialise the graph or wait for the cycle to close — you spot one node that is the meeting point of two rw-edges.

Fekete’s theorem (2005): in any non-serializable SI execution, the cycle contains two consecutive rw-edges — a T-pivot with an inbound rw-edge from a T-in and an outbound rw-edge to a T-out, both concurrent with the pivot. 2

Necessary… Every real anomaly contains the dangerous structure. Catch the pattern → catch every write skew.

…not sufficient The pattern also appears in perfectly serializable histories (the cycle never closes, or the third edge runs the other way).

So a detector is conservative Aborting on the pattern catches every anomaly and some innocents — by design.

The payoff Cheap-and-conservative beats exact-and-expensive: no full graph, no cycle confirmation. That’s what makes §4 possible.

✗ “Seeing the dangerous structure means a cycle definitely exists”
✓ It’s necessary, not sufficient — the would-be cycle may never close

⚠️ Memory rule: Dangerous structure = one pivot with an inbound AND outbound rw-edge; necessary for every anomaly, sufficient for none.

Memory check
  • What two edges meet at the pivot? → one inbound rw, one outbound rw
  • Necessary or sufficient? → necessary for an anomaly, not sufficient
  • How many txns must you break to be safe? → just one of the three
4

How PostgreSQL catches it — SIReads & a conservative abort

Detect rw-antidependencies as they form, without ever making a read block.

SSI detection at runtime. Every read under SERIALIZABLE leaves a non-blocking SIRead lock recording what was read, escalating from tuples to pages to relations under pressure. A later write landing on SIRead-locked data flags an rw-conflict and sets an in/out flag. When one transaction has BOTH an inbound and outbound flag — the dangerous structure — the engine aborts a transaction (preferring the pivot) with SQLSTATE 40001. SIRead locks survive the reader's commit.

SSI detection at runtime. Every read leaves a non-blocking SIRead “lock” recording what was touched. A later write landing on SIRead-locked data is a detected rw-conflict and sets an in/out flag. Both flags on one txn → the engine aborts (preferring the pivot) with SQLSTATE 40001. SIRead locks survive the reader’s commit — a conflicting writer can still arrive.

PieceWhat it does
SIRead locks

Predicate locks taken on what a serializable read touched. They never blocka writer — they only leave a record “this txn read this.” A write later falling on one = a detected rw-edge.

Granularity

Tuple → page → relation as a txn reads more; index-range locks cover predicates, which is how SSI catches phantoms (rows that don’t exist yet but would match). Coarser = less memory, more conflicts.

Abort rule

Each txn carries an inbound-conflict flag and an outbound-conflict flag. When both set on one txn (the pivot), roll one back — preferring the pivot itself, unless it already committed.

Locks outlive reader

A SIRead lock is retained after the reading txn commits, because a writer creating the dangerous structure can show up later. This is why long, read-heavy txns inflate the footprint.

The contract On 40001 ( could not serialize access due to read/write dependencies), retry the transaction. Non-negotiable.

Why retry is safe Idempotency makes the retry harmless — see the Idempotency Keys deep dive. An app that doesn’t retry is broken, not unlucky.

Conservative, again Fires on the structure, not a confirmed cycle, so every genuine write skew is caught — plus a few innocents.

Citation Cahill et al. (SIGMOD 2008) gave the algorithm; Ports & Grittner (VLDB 2012) shipped it in PostgreSQL. 1 3

✗ “SIRead locks block writers like real locks”

✓ They never block — they only record reads so a later write reveals an rw-edge

🔍 Memory rule: Reads leave non-blocking SIRead marks; a write on a marked datum is an rw-edge; both flags on one txn → abort with 40001 → retry.

Memory check
  • Do SIRead locks block writers? → no; they only record what was read
  • What's the abort SQLSTATE, and the app's duty? → 40001; retry the transaction
  • Why retain a SIRead lock past commit? → a conflicting writer can arrive later
  • How does SSI catch phantoms? → index-range / predicate locks
5

The price, the tuning, and the escape hatch

SSI buys serializability at ~SI throughput. “Roughly” and “conservative” each cost something — senior use means knowing the bill.

The trade-off and the fast path. Versus strict 2PL (blocks readers, deadlocks) and plain SI (fast but admits write skew), SSI keeps reads non-blocking and stays correct, paying instead in false-positive aborts and predicate-lock memory. The escape hatch: a READ ONLY transaction proven to a safe snapshot can neither suffer nor cause an anomaly, so PostgreSQL runs it as plain REPEATABLE READ with zero SSI overhead.

The trade-off, and the fast path. Vs strict 2PL (blocks readers, deadlocks) and plain SI (fast, admits write skew), SSI keeps reads non-blocking and stays correct — paying in false-positive aborts and predicate-lock memory. A READ ONLY txn proven to a “safe snapshot” runs as plain REPEATABLE READ, zero SSI overhead.

Strict 2PLPlain SISSI
Correct (serializable)YesNo — write skewYes
Reads block writersYesNoNo
Failure modeDeadlocks, latencySilent anomaliesFalse-positive aborts
StancePessimistic (block up front)Optimistic (abort at the end)

False positives Rise with contention and with coarse predicate locks — as SIReads summarize tuple→page→relation, unrelated rows collide and innocents abort.

Predicate-lock memory Bounded by max_pred_locks_per_transaction × connections. Past it, Postgres summarizes to coarser granularity (more false positives) rather than failing.

The oldest txn sets the cost One long serializable txn holds its SIRead locks the whole time — same way a long txn starves VACUUM in MVCC.

Safe-snapshot escape hatch A read-only txn that can’t be part of any dangerous structure drops to REPEATABLE READ. READ ONLY DEFERRABLE waits for that snapshot on purpose. 3

✗ “Each node serializable ⇒ the sharded fleet is serializable”

✓ Postgres SSI is single-node; distributed serializability is a harder problem

💸 Memory rule: 2PL pays up front in blocking; SSI pays at the end in conservative aborts — for read-mostly, low-conflict OLTP, optimism wins.

Memory check
  • Two costs SSI pays? → false-positive aborts + predicate-lock memory
  • What does READ ONLY DEFERRABLE buy a long report? → a free, abort-proof safe snapshot
  • Where does optimism lose to 2PL? → high contention — measure the crossover
  • Scope of the guarantee? → single node only

Interview — pick your bar

Answer out loud in ~60s, then reveal. Core = recall · Senior = trade-offs & failure modes · Staff = synthesis under ambiguity · System Design = open design round (a different axis, not a harder level).

Snapshot isolation prevents dirty reads and lost updates. So what does it not prevent, and why?
Hit these points: write skew — two txns read an overlapping condition and each write a different row → SI's first-committer-wins only catches same-row write-write conflicts, so neither write is blocked → both commit and together break an invariant no serial order could produce → name a concrete instance: last unit of inventory, double-booked room, both on-call doctors going off shift → the one-liner: "SI's only hole is write skew — read a shared condition, write different rows."
What is serializable snapshot isolation, in one breath?
Hit these points: SSI runs every txn on ordinary MVCC snapshots — reads stay non-blocking, exactly like SI → on top of that it additionally tracks read/write dependencies between concurrent txns at runtime → when it spots the pattern that can cause write skew (the dangerous structure of rw-antidependencies), it aborts one txn rather than let the anomaly commit → so it delivers true serializability with SI's read performance, paying in aborts instead of read locks → "SI plus a runtime detector that aborts the txn that would break serializability."
What is an rw-antidependency, and why is it the only edge SI can't prevent?
Hit these points: an rw-antidependency is "T1 reads a version of x, then T2 writes a newer version of x" — T1 read stale, so in any serial order T1 must come before T2 → SI already prevents ww (first-committer-wins) and cross-txn wr (you only read committed snapshot versions) → but a write that lands after your frozen snapshot is invisible to you, yet it still orders you first — SI has no way to notice it → hence the only cycles SI can produce are made of rw-edges → that's why SSI watches rw-edges specifically and nothing else.
Define serializability precisely enough that you could build a detector from the definition.
Hit these points: model each committed txn as a node → draw a directed edge whenever one txn must precede another in any equivalent serial order (wr = read another's write, ww = write-write order, rw = read-then-overwritten) → a history is serializable iff the resulting dependency graph is acyclic → a cycle means each txn is both before and after the others, so no serial order exists → the detector's only job: don't let a cycle commit → this is the conflict-serializability / dependency-graph framing the whole SSI argument rests on.
State Fekete's theorem and explain why it makes SSI cheap.
Hit these points: every non-serializable SI history contains two consecutive rw-edges meeting at a pivot txn — an inbound rw from T-in and an outbound rw to T-out, with the pivot concurrent with both → that local two-edge shape (the "dangerous structure") is necessary for any SI anomaly → so the engine never has to build the full graph or confirm a cycle actually closed — it just watches each txn for one inbound + one outbound rw flag → break any one of the three edges and the cycle can't form → that's the whole reason SSI is cheap: detect a local pattern, not a global cycle.
Why does SSI abort transactions that were actually serializable — and is that a bug?
Hit these points: the dangerous structure is necessary for an anomaly but not sufficient — the same two-edge shape also appears in histories where the cycle never closes and the result is serializable → aborting on the shape alone is deliberately conservative: it catches every real anomaly plus some innocents (false positives) → that's the price of detecting a local pattern instead of proving a global cycle, which would be far more expensive → so it's a designed trade-off, not a bug → the false-positive rate climbs with contention and with coarse predicate-lock granularity, which is exactly what you tune.
How does PostgreSQL detect an rw-edge at runtime without making reads block, and what must the app do?
Hit these points: SIRead predicate locks — non-blocking records of what each serializable read touched (tuple → page → relation granularity, plus index-range locks to catch phantoms) → they take no real lock, so they never block a writer; they just leave a trace → a write landing on a SIRead-locked datum is a detected rw-conflict, flagged on both txns involved → when one txn ends up with both an inbound and an outbound rw flag (the pivot), the engine aborts it with SQLSTATE 40001 → the app contract: retry on 40001, made safe by idempotent transactions → note SIRead locks outlive the reader's commit, because a later writer can still close the cycle.
A serializable workload is aborting too often under load. Walk me through your tuning levers.
Hit these points: keep txns short and reads selective — this cuts both false positives and predicate-lock pressure, and it's the highest-leverage fix → raise max_pred_locks_per_transaction to delay summarization, because once SIRead locks summarize up to coarse (page/relation) granularity, false positives spike → move long reporting reads to READ ONLY DEFERRABLE for a free safe snapshot that can't be aborted → watch the oldest open txn — like VACUUM, it sets the cost → if contention is genuinely high, measure the crossover where 2PL's blocking tax beats SSI's abort tax, rather than assuming SSI always wins.
Compare SI, SSI, and 2PL on what they guarantee and how they fail — and when you'd pick each.
Hit these points: SI — consistent snapshot, first-committer-wins; fast non-blocking reads but leaks write skew; pick it when no cross-row invariant is at risk or you can materialize the conflict → SSI — SI plus rw-dependency tracking; true serializability with non-blocking reads, fails by aborting (40001) and consumes predicate-lock memory; pick it for invariant-critical workloads at low-to-moderate contention with a retry loop → 2PL — locks held to commit; serializable by blocking, fails by waiting and deadlocks; pick it (or explicit FOR UPDATE) at high contention where abort-and-retry would thrash → synthesis: the choice is "fail by aborting (optimistic, SSI) vs fail by blocking (pessimistic, 2PL)," decided by contention; measure the crossover, don't assume.
Your fleet is sharded and each Postgres node runs SERIALIZABLE. Is the system serializable?
Hit these points: no — PostgreSQL SSI guarantees serializability within one server only → it tracks rw-dependencies through that node's own SIRead locks; it has no visibility into reads/writes on another shard, so a cycle that spans shards is invisible to every node → the guarantee does not extend across shards or logical-replication boundaries for free → distributed serializability is a separate, harder problem requiring a global commit order (consensus / a distributed SSI, as in CockroachDB/Spanner-style systems) → the staff move: state the scope of the guarantee explicitly, and don't let "each node is serializable" be mistaken for "the system is serializable."
A team wants to default the whole OLTP service to SERIALIZABLE to "never think about anomalies again." Make the principal call.
Hit these points: don't decide by slogan — characterize the workload: contention level, txn duration, which txns actually carry cross-row invariants → upside: Serializable removes the whole anomaly class, and on SSI reads stay non-blocking, so at low contention the cost is modest → but the cost is real and workload-dependent: abort rate (and false positives) rises with contention, predicate-lock memory grows, and every serializable txn now needs a correct 40001 retry loop or you've shipped a latent failure → and the guarantee is single-node, so it gives nothing across shards → the principal call: apply Serializable to the specific txns whose invariants need it, keep the rest at the lowest sufficient level, mandate retry + idempotency, and gate the rollout on measured abort rate with a kill criterion → make it a measured decision, not a global flag flipped on faith.
Design-round framework — drive any serializable-vs-SI / concurrency-control prompt through these, out loud:
  1. Invariants at risk — which cross-row conditions could write skew break, with a concrete instance.
  2. Contention & txn shape — hot keys, read selectivity, txn duration (sets abort vs block cost).
  3. Mechanism choice — SI + materialized lock, SSI/Serializable, or explicit 2PL (FOR UPDATE).
  4. Abort/retry contract — 40001 retry loop + idempotency for every serializable path.
  5. Predicate-lock budget — granularity, summarization, max_pred_locks_per_transaction, safe snapshots for long reads.
  6. Scope — single-node only; what changes if the data is sharded.
  7. Observability — abort rate, oldest open txn, predicate-lock usage; alert on abort storms.
Choose a concurrency-control design for a hospital on-call scheduler that must always keep at least one doctor on call.
A strong answer covers: name the hazard — this is the canonical write-skew case: two doctors each read "another is still on call" and each go off shift, dropping the count to zero → clarify scale: low write volume, a hard invariant, correctness dominates throughput → option A, the cheap fix: materialize the conflict into a single lockable row (an on-call count or a guard row) and take SELECT … FOR UPDATE before going off shift, so the two txns serialize on one row → option B: run the go-off-shift txn at SERIALIZABLE so SSI's rw-tracking aborts the loser, with a 40001 retry loop → given low contention either works; prefer SSI if there are many such invariants and you don't want to hand-place locks, prefer the materialized lock if it's one hot invariant → reserve fixed effort for idempotent retries → trade-off: explicit lock (predictable, blocks, must remember to take it) vs SSI (declarative, non-blocking reads, aborts under contention) → note the guarantee is single-node, so a sharded scheduler needs a global ordering.
Design isolation for a service that runs short invariant-critical writes and long analytical reads against the same database, choosing among SI, SSI, and 2PL.
A strong answer covers: partition by workload — the two paths have opposite needs and shouldn't share one global level → the short invariant-critical writes are where write skew bites, so run those at SERIALIZABLE (SSI) with a 40001 retry loop, or pin the conflict with FOR UPDATE if contention is high enough that aborts would thrash → the long analytical reads must not be aborted and must not pin cleanup: run them as READ ONLY DEFERRABLE to get a safe snapshot with zero SSI overhead, or route them to a read replica → keep write txns short and reads selective to bound predicate-lock memory and false positives → watch the crossover: if write contention is genuinely high, 2PL's blocking may beat SSI's abort churn — measure it → observability: track abort rate, predicate-lock usage, and the oldest open txn → trade-offs: SSI (non-blocking reads, aborts) vs 2PL (no aborts, blocks + deadlocks); safe-snapshot reads (free, must be read-only) vs replica reads (isolated, but lag); and the reminder that all of it is single-node.

Retrieval practice — mix the levels

Reading builds fluency (feels familiar). Recall builds storage (lasts). Answer from memory — instant feedback.

Q1. Snapshot isolation permits write skew specifically because…

Q2. A multiversion history is serializable exactly when its dependency graph…

Q3. Fekete's theorem says every snapshot-isolation anomaly contains…

Q4. SSI may abort a transaction that was actually serializable because…

Q5. PostgreSQL's READ ONLY DEFERRABLE / safe-snapshot optimization lets…

Q6. A SIRead lock differs from an ordinary lock because it…

Reconstruct the argument from a blank page

Write the five moves in order, one line each, with nothing on screen. Then reveal and compare — gaps are your next drill.


1 SI’s only hole is write skew (read condition, write different rows) · 2 serializable ⟺ acyclic dependency graph; SI’s only cycles are rw-edges · 3 Fekete: every anomaly has a pivot with an inbound + outbound rw-edge (necessary, not sufficient) · 4 Postgres: non-blocking SIRead locks → both flags on a pivot → abort with 40001 → retry · 5 price = false-positive aborts + predicate-lock memory; escape = READ ONLY DEFERRABLE; single-node only.

I’m your teacher — ask me anything. Say “grill me on SSI” for mixed no-clue questions, or drop a real schema and I’ll walk you through where write skew hides in it and whether SERIALIZABLE or an explicit lock is the right fix. Want the next deep dive — MVCC, or distributed serializability? Just ask.

★ Cheat sheet — term → engineering

Mental models: Write skew = read a shared condition, write different rows · Serializable = acyclic dependency graph · rw-antidependency = read-then-someone-writes-later, the only edge SI can’t stop · Dangerous structure = one pivot, two rw-edges (necessary, not sufficient) · SIRead lock = a non-blocking read receipt · 40001 = retry · Safe snapshot = abort-proof read-only.

Translation table

TermWhat it actually is
Snapshot isolation

Read a frozen consistent snapshot; first-committer-wins on same-row writes — not serializable

Write skew

Read overlapping condition → each writes a different row → both commit → invariant broken

Dependency graphNodes = txns, edges = must-precede (wr / ww / rw); serializable ⟺ acyclic
rw-antidependencyT1 reads old x, T2 writes newer x; T1 must come first — SI can’t prevent it
Dangerous structureA pivot with both an inbound and outbound rw-edge; necessary, not sufficient
SIRead lockNon-blocking predicate lock recording a read; outlives the reader’s commit
SQLSTATE 40001”could not serialize access” → the app must retry the transaction
READ ONLY DEFERRABLEWait for a safe snapshot, then run as REPEATABLE READ with zero SSI cost

Three rules for the principal chair

① SERIALIZABLE is the cheap-and-conservative bet: it will abort under contention — so always retry on 40001. ② Keep txns short and reads selective to cut false positives and lock memory; push long reads to READ ONLY DEFERRABLE. ③ The guarantee is single-node — don’t assume a sharded fleet is serializable because each node is.

Sources
1. Cahill, Röhm & Fekete, Serializable Isolation for Snapshot Databases (SIGMOD 2008; ACM TODS 2009). The original SSI algorithm — track rw-antidependencies, abort on the dangerous structure.
2. Fekete, Liarokapis, O'Neil, O'Neil & Shasha, Making Snapshot Isolation Serializable (ACM TODS 2005). The theorem that every SI anomaly has two consecutive rw-edges at a pivot.
3. Ports & Grittner, Serializable Snapshot Isolation in PostgreSQL (VLDB 2012) — arXiv:1208.4179. SIRead predicate locks, granularity/summarization, crash recovery, 2PC, and the safe-snapshot read-only optimization.
4. PostgreSQL docs, Transaction Isolation → Serializable Isolation Level; the Serializable wiki page; the in-tree README-SSI.
5. Berenson, Bernstein, Gray et al., A Critique of ANSI SQL Isolation Levels (SIGMOD 1995). Why named-SERIALIZABLE-that-is-really-SI (e.g. Oracle) admits write skew.
6. Adya, Weak Consistency: A Generalized Theory and Optimistic Implementations (MIT PhD, 1999). The multiversion dependency-graph framework the whole argument rests on. Synthesis in Kleppmann, DDIA, Ch. 7.
Was this lesson helpful? Thanks — noted.

These notes reflect my current understanding and are updated as I learn, build, and discover better explanations.