Isolation Levels

Imagine two bank clerks processing transfers on the same account at the same instant. One reads a balance of $1000, the other reads the same $1000. Both compute new balances from that stale number. One clerk's update silently vanishes. This is not a hypothetical bug. It is the default behavior of many production databases. This happens unless someone deliberately chooses a stronger isolation level. The gap between "transactions look isolated" and "transactions are literally executed one at a time" is exactly the space isolation levels are designed to manage. Full serializability guarantees every concurrent execution is equivalent to some serial order. It is the gold standard of correctness. But enforcing it strictly (e.g., via strict Two-Phase Locking) can devastate throughput. It blocks readers behind writers and writers behind readers. Real systems therefore expose a menu of weaker, cheaper isolation levels. Each permits a precisely characterized set of anomalies in exchange for more concurrency. Choosing an isolation level is a contract between the application programmer and the database. The database promises to prevent at least the anomalies the level forbids. The programmer promises to tolerate whatever it doesn't. The SQL standard defines four levels: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE. It defines them through the anomalies they disallow: dirty reads, non-repeatable reads, and phantom reads. This anomaly-based definition is deceptively simple. As we'll see, it does not perfectly match what modern engines actually implement. This is especially true once Multiversion Concurrency Control enters the picture. Systems like PostgreSQL, Oracle, and MySQL/InnoDB each interpret "REPEATABLE READ" slightly differently. Understanding why requires understanding the underlying locking and versioning mechanisms. It is not enough to just look at the anomaly table. This article gives a precise, mechanistic account. It covers what each anomaly actually is, at the level of read/write operations on data items. It shows how each isolation level is implemented, via locking and/or MVCC snapshots. It includes a full worked schedule trace showing an anomaly occurring and then being prevented. Finally, it covers the pitfalls that trip up engineers. These are engineers who assume "isolation level" means "serializability lite." In reality, it means "a specific, weaker guarantee with sharp edges."

Key takeaways
  • Isolation levels trade correctness (closeness to serializable execution) for concurrency (fewer blocked transactions).
  • Three canonical anomalies define the SQL standard levels: dirty read, non-repeatable read, phantom read.
  • READ UNCOMMITTED allows all three; READ COMMITTED blocks dirty reads only; REPEATABLE READ blocks dirty and non-repeatable reads; SERIALIZABLE blocks all three.
  • Locking implementations use lock duration and granularity (row locks vs. predicate/gap locks) to prevent each anomaly.
  • MVCC implementations (snapshot isolation) prevent all three named anomalies without blocking readers, yet are not truly serializable — they permit write skew.
  • The choice of isolation level is an explicit tradeoff every application must make; it directly determines what bugs are possible, foreshadowing how Log-Based Crash Recovery must also respect these same consistency guarantees after a crash.
The core insight

An isolation level is a formal promise about which interleavings of concurrent transactions are forbidden, letting engineers trade strict serializability for concurrency in a precisely bounded, contractual way.

The idea

A schedule is an interleaving of the read/write operations of concurrently executing transactions. A schedule is serializable if it produces the same result as some serial (one-at-a-time) execution of those same transactions — this is the correctness criterion inherited from Serializability and Schedules. Enforcing serializability exactly, however, generally requires transactions to block each other (via two-phase locking) or to abort and retry (via optimistic/validation schemes), and both cost throughput. Isolation levels are named degrees of relaxation from strict serializability, each defined by which of three classical anomalies it permits. The three anomalies are defined precisely on read/write operations over data items, assuming T1,T2T_1, T_2 are concurrent transactions:

  • Dirty read: T1T_1 writes a value to item XX; T2T_2 reads that uncommitted value; T1T_1 later aborts. T2T_2 has now read a value that officially never existed.
  • Non-repeatable read (also called a fuzzy read): T1T_1 reads item XX; T2T_2 updates and commits XX; T1T_1 reads XX again and gets a different value, within the same transaction.
  • Phantom read: T1T_1 reads a set of rows satisfying some predicate PP (e.g., `WHERE balance > 1000`); T2T_2 inserts (or deletes) a row that now satisfies (or no longer satisfies) PP and commits; T1T_1 re-runs the query and sees a different set of rows — a "phantom" that wasn't there before, or one that vanished.

The SQL-92 standard defines four isolation levels purely in terms of which of these anomalies are permitted:

Isolation LevelDirty ReadNon-repeatable ReadPhantom Read
READ UNCOMMITTEDallowedallowedallowed
READ COMMITTEDpreventedallowedallowed
REPEATABLE READpreventedpreventedallowed
SERIALIZABLEpreventedpreventedprevented

A crucial subtlety: this table describes a necessary condition, not a sufficient one for serializability. A schedule can prevent all three named anomalies and still not be serializable — the classic counter-example is write skew under snapshot isolation, discussed below. The anomaly table is a useful, standardized vocabulary, but "SERIALIZABLE" is the only level in the table that is defined to equal true serializability; the others are defined purely by exclusion of specific anomalies, leaving room for anomalies nobody bothered to name in 1992.

How it works

There are two families of mechanism used to implement isolation levels: lock-based (rooted in Two-Phase Locking) and version-based (rooted in Multiversion Concurrency Control). Understanding both is necessary because different production systems default to different families, and the guarantees differ subtly even when the SQL-level name is the same.

Lock-based implementation

The standard mapping from isolation level to locking discipline varies lock duration — how long a lock is held — and lock scope — whether it protects only existing rows or also protects a predicate against phantom inserts. ``` ISOLATION LEVEL -> LOCKING PROTOCOL (2PL-based) READ UNCOMMITTED:

  • Write locks: acquired before write, held until commit/abort (long duration)
  • Read locks: NONE acquired at all

-> reads never block, and can see uncommitted writes => dirty reads possible READ COMMITTED:

  • Write locks: held until commit/abort (long duration)
  • Read locks: acquired before read, released IMMEDIATELY after the read

(not held until end of transaction) -> a read can never see an uncommitted write (dirty read prevented) -> but re-reading X later may see a different, newly committed value (non-repeatable read still possible) because the read lock was already released REPEATABLE READ:

  • Write locks: held until commit/abort
  • Read locks: held until commit/abort (long duration, like writes)

-> once T reads X, no other transaction can modify X until T finishes (non-repeatable read prevented) -> BUT ordinary row locks say nothing about rows that do not exist yet; a concurrent INSERT matching T's earlier predicate is unaffected (phantom read still possible) SERIALIZABLE:

  • Write locks: held until commit/abort
  • Read locks: held until commit/abort
  • PLUS: predicate locks / index-range ("gap") locks / next-key locks

taken on the predicate of every range query, blocking inserts or deletes that would change the result set -> phantoms are prevented because the predicate itself is locked, not just the rows that happened to exist at read time ``` The key engineering insight is that ordinary row-level locking can never prevent phantoms, because you cannot lock a row that doesn't exist yet. Preventing phantoms requires locking the space of possible rows — implemented in practice as next-key locking (locking a row plus the gap before it, as in InnoDB) or true predicate locking (locking the condition of a `WHERE` clause, as in some index-based schemes). This is the mechanistic reason phantom prevention is qualitatively harder than the other two anomalies and is reserved for the strictest level.

Version-based implementation (Snapshot Isolation)

Most modern systems (PostgreSQL, Oracle, SQL Server's `READ COMMITTED SNAPSHOT`, MySQL/InnoDB) implement REPEATABLE READ and often what they call "SERIALIZABLE" using snapshot isolation (SI), built on MVCC rather than blocking locks. ``` SNAPSHOT ISOLATION (informal protocol) on BEGIN(T): snapshot_time(T) := current commit-counter // T will only ever see versions of rows committed before snapshot_time(T) on READ(T, X): return the version of X with the largest commit-timestamp that is <= snapshot_time(T) // never blocks; never sees uncommitted or "future" writes on WRITE(T, X): buffer the write locally (not visible to other transactions yet) on COMMIT(T): if EXISTS T' such that: T' committed after snapshot_time(T) AND T' wrote to some item X that T also wrote then ABORT T // "first committer wins" -- prevents lost updates else assign T a new commit-timestamp make all of T's writes visible at that timestamp COMMIT ``` Because every read is served from a private, fixed-in-time snapshot, dirty reads, non-repeatable reads, and phantom reads are all impossible under SI: nothing committed after TT's snapshot is ever visible to TT, whether it's a modified existing row or a newly inserted one. This is why systems that use SI market it as "REPEATABLE READ" or even "SERIALIZABLE" — it does dominate the SQL-92 anomaly table. But SI is not equivalent to serializability. The canonical failure is write skew: two transactions each read an overlapping but disjoint set of items, each writes to a different item based on what it read, and neither write conflicts with the other under the "first committer wins" rule — yet the combined effect could never have arisen from any serial execution. ``` WRITE SKEW EXAMPLE (on-call doctors) Invariant the application relies on: at least one of {Alice, Bob} is on call. Initial state: alice.on_call = true, bob.on_call = true T1 (Alice goes off call): T2 (Bob goes off call): snapshot: both on_call = true snapshot: both on_call = true read bob.on_call -> true read alice.on_call -> true (Alice may go off call, since Bob covers) (Bob may go off call, since Alice covers) write alice.on_call = false write bob.on_call = false COMMIT (no overlapping WRITE set COMMIT (no overlapping WRITE set with T2 -> succeeds) with T1 -> succeeds) Final state: alice.on_call = false, bob.on_call = false INVARIANT VIOLATED -- nobody is on call. ``` Both commits succeed under SI's write-write conflict check because T1T_1 never writes to `bob.on_call` and T2T_2 never writes to `alice.on_call` — their write sets don't intersect, so "first committer wins" never triggers. Yet no serial order of T1,T2T_1, T_2 produces this final state. This is why true SQL SERIALIZABLE, when implemented over MVCC, requires an additional mechanism — Serializable Snapshot Isolation (SSI) — which tracks read-write ("rw") anti-dependencies between transactions and aborts one of them when a dangerous structure (a cycle risk) is detected, on top of the plain SI protocol.

Figure
Isolation levels ordered by strength, and the anomalies each permits
strength ↑ ↓ weak READ UNCOMMITTED no read locks; dirty writes visible permits: dirty, non-repeat, phantom READ COMMITTED short read locks / fresh snapshot per stmt permits: non-repeat, phantom REPEATABLE READ long read locks / one snapshot per txn permits: phantom SERIALIZABLE predicate/gap locks or SSI conflict graph permits: nothing Anomalies (read/write on item X) Dirty Read T1 writes X (uncommitted) → T2 reads X → T1 aborts Non-repeatable Read T1 reads X → T2 writes+commits X → T1 re-reads X, differs Phantom Read T1 reads set matching P → T2 inserts row matching P, commits → T1 re-runs query, row-set differs Write Skew (SI-only anomaly) T1, T2 read disjoint write-targets from a shared snapshot; each writes based on stale peer data; both commit, invariant across X,Y is broken. Not caught by plain Snapshot Isolation!

Complexity & correctness

Time/space cost is a function of lock duration and lock scope, not of the algorithm's asymptotic complexity — isolation levels are a concurrency-control policy layered on the same O(1)-per-operation lock table or MVCC version chain. The right way to reason about "complexity" here is in terms of concurrency (blocking probability, abort rate) rather than big-O, since the underlying data structures (lock tables, version chains) are shared across all levels.

  • READ UNCOMMITTED: minimal locking overhead — read locks are never taken, so readers never block and writers only block other writers. Highest throughput, weakest guarantee.
  • READ COMMITTED: read locks are short-lived (held only for the duration of a single read), so the window during which a reader can block a writer (or vice versa) is minimized. Under MVCC variants, each statement gets a fresh snapshot, so cost is one snapshot-read per statement — cheap, no blocking on reads at all.
  • REPEATABLE READ: read locks are held for the transaction's entire lifetime, so long transactions accumulate long-held locks, increasing the chance of blocking and deadlock. Under MVCC, cost is a single snapshot per transaction (not per statement) — still no read blocking, but the abort rate for conflicting writers rises, since "first committer wins" checks are made against a longer-lived reference point.
  • SERIALIZABLE: adds predicate/gap locks (2PL route) or full read-write conflict tracking via SSI (MVCC route). The 2PL route can suffer from lock escalation and reduced concurrency on hot ranges; the SSI route adds bookkeeping to detect "dangerous structures" (pairs of rw-antidependency edges forming a cycle risk), at the cost of a higher abort-and-retry rate under contention, but with no blocking added to reads.

Correctness argument, sketch. For 2PL-based mechanisms, correctness follows directly from the standard 2PL theorem: any schedule produced by a protocol in which every transaction acquires all its locks before releasing any (two-phase behavior) is conflict-serializable. Adding predicate/gap locks extends the item space being locked from "existing rows" to "the query predicate as a region," which brings phantom-inducing inserts/deletes under the same 2PL discipline — the same proof applies because a phantom insert is, in this model, just another conflicting operation on a locked resource (the gap). For MVCC-based Snapshot Isolation, the argument is different and subtler. SI guarantees that every transaction reads from a single consistent point-in-time snapshot, so within one transaction, repeated reads of the same item are always equal (no non-repeatable reads), and any predicate query re-executed against the same snapshot always returns the same row set (no phantoms). Formally, SI is equivalent to serializability if and only if the transaction workload contains no pair of transactions with two rw-antidependency edges forming a specific cyclic pattern (a "pivot" transaction with both an incoming and an outgoing rw-edge in the same cycle) — this is the theoretical result underlying SSI: detect that specific structural pattern in the dependency graph at runtime and abort one participant, and the resulting schedule is provably serializable. Plain SI, lacking this check, is provably not serializable in general — write skew is the standard counterexample proving this negative result.

Honest tradeoff summary: weaker isolation levels never increase the number of possible final states beyond what's reachable from careful application-level reasoning about which anomalies are tolerable, but they place the burden of correctness partly on the application programmer rather than entirely on the database. Every level below SERIALIZABLE is a bet that the application either doesn't hit the permitted anomaly in practice, or handles it defensively (e.g., with explicit `SELECT ... FOR UPDATE` locking hints).

Worked example

Consider two concurrent transactions on a `accounts(id, balance)` table, starting state `balance(A) = 500`, running under READ COMMITTED, then re-run under SERIALIZABLE to see the difference.

Schedule under READ COMMITTED (dirty read prevented, non-repeatable read demonstrated):

StepT1T_1 (audit report)T2T_2 (transfer)State of `A.balance`Notes
1`BEGIN`500
2`READ A.balance` → 500500T1T_1's first read
3`BEGIN`500
4`UPDATE A SET balance = balance - 100`400 (uncommitted)write lock held by T2T_2
5`READ A.balance` (attempted)—blocks: READ COMMITTED takes a read lock, which conflicts with T2T_2's write lock. No dirty read occurs.
6`COMMIT`400 (committed)write lock released
7(unblocks) `READ A.balance` → 400400T1T_1's second read succeeds, sees the new committed value
8`COMMIT`400

T1T_1 read `500` then `400` for the same row within the same transaction — a textbook non-repeatable read. This is legal under READ COMMITTED (only dirty reads are forbidden) and illustrates exactly why an "audit report" transaction that reads a value twice and expects consistency needs a stronger level.

Same schedule under REPEATABLE READ (locking implementation):

StepT1T_1T2T_2StateNotes
1`BEGIN`500
2`READ A.balance` → 500500T1T_1 acquires a long-held read (shared) lock on `A`
3`BEGIN`500
4`UPDATE A SET balance = balance - 100` (attempted)500blocks: T2T_2's write lock request conflicts with T1T_1's still-held read lock
5`READ A.balance` → 500(still blocked)500T1T_1's second read: identical to the first — repeatable
6`COMMIT`(unblocks)500T1T_1 releases its read lock at commit
7`UPDATE ... ` succeeds, `COMMIT`400T2T_2 now proceeds, serialized after T1T_1

The resulting schedule is equivalent to the serial order T1→T2T_1 \to T_2 — REPEATABLE READ achieved this by making T2T_2 wait, trading concurrency (an extra block) for the stronger guarantee.

Phantom-read illustration (why REPEATABLE READ alone is not enough):

StepT1T_1 (report: count large accounts)T2T_2 (new account)
1`SELECT COUNT(*) FROM accounts WHERE balance > 1000` → 5
2`INSERT INTO accounts VALUES (99, 5000)`; `COMMIT`
3`SELECT COUNT(*) FROM accounts WHERE balance > 1000` → 6

Under row-level REPEATABLE READ locking, T1T_1 only held locks on the 5 rows it actually read in step 1 — row `99` did not exist yet, so there was nothing to lock, and T2T_2's insert sails through. Under SQL SERIALIZABLE with predicate/next-key locking, T1T_1's range query in step 1 locks the gap covering `balance > 1000`, and T2T_2's insert in step 2 blocks until T1T_1 commits — eliminating the phantom.

Common pitfalls & traps

PitfallWhy it happensConsequence
Assuming "REPEATABLE READ" means the same thing across databasesMySQL/InnoDB's REPEATABLE READ actually prevents phantoms too (via next-key locking), unlike the SQL-92 minimumCode tested on one engine silently changes behavior when ported
Treating snapshot isolation as full serializabilitySI prevents all three named anomalies but not write skewInvariants spanning two rows (constraints, counters, "at least one of") silently break under load
Using default isolation level without reading itMany drivers default to READ COMMITTED silentlyReports "flicker" between reads; off-by-one totals in analytics queries
Forgetting phantom prevention needs range/gap locks, not row locksRow-level 2PL has nothing to lock for rows that don't exist yetRange queries (`COUNT`, `SUM ... WHERE`) return inconsistent aggregates across re-execution
Relying on isolation level alone instead of explicit locking (`SELECT FOR UPDATE`) for read-modify-writeApplication assumes the isolation level "does the right thing" automaticallyClassic lost-update bug even under REPEATABLE READ if the read and write are logically two separate statements without locking hints
Long transactions under lock-based REPEATABLE READ/SERIALIZABLELocks are held for the transaction's full lifetimeLock contention, deadlocks, and reduced throughput scale badly with transaction length

Pioneers & history

Isolation levels emerged from the theory of concurrency control developed for early distributed and multi-user database systems in the 1970s, and were later crystallized into a critical, corrective paper once vendors' informal terminology diverged from what their engines actually did.

Origins · 1 minJim Gray and how this was discovered
JG
Jim Gray
1944–2007 (disappeared at sea) · IBM Research and Tandem Computers, transaction processing pioneer

Jim Gray formalized the theory of transactions, locking granularity, and degrees of consistency in the 1970s, work that directly underlies the SQL isolation-level hierarchy. His 1975 paper on locking granularity and his later collaboration on "A Critique of ANSI SQL Isolation Levels" (1995) exposed that vendors' claimed isolation levels did not match their formal anomaly definitions — motivating precise, mechanism-based definitions (like snapshot isolation) rather than purely anomaly-name-based ones. He received the Turing Award in 1998 for foundational contributions to database and transaction-processing research.

History
1976
— Jim Gray and colleagues formalize "degrees of consistency" and locking granularity for System R, the ancestor of the modern isolation-level hierarchy.
1981
— Two-phase locking and serializability theory (Eswaran, Gray, Lorie, Traiger) become the standard correctness criterion for concurrent schedules.
1992
— SQL-92 standard defines READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE via the dirty/non-repeatable/phantom anomaly table.
1995
— Berenson, Bernstein, Gray, et al. publish "A Critique of ANSI SQL Isolation Levels," showing the standard's anomaly-based definitions are ambiguous and introducing snapshot isolation as a rigorously defined alternative.
2008
— Cahill, Röhm, and Fekete publish Serializable Snapshot Isolation (SSI), giving a practical algorithm to upgrade snapshot isolation to true serializability, later adopted by PostgreSQL (9.1, 2011).

Where this matters

Every mainstream relational database ships a configurable isolation level, and the choice materially affects both correctness and throughput in production systems. PostgreSQL implements READ COMMITTED (its default) and REPEATABLE READ via snapshot isolation, and offers true SERIALIZABLE via SSI for applications with cross-row invariants. Oracle Database has historically offered only READ COMMITTED and SERIALIZABLE, both snapshot-based, deliberately omitting the SQL-92 REPEATABLE READ name because Oracle's snapshot-based SERIALIZABLE does not use predicate locking (users must be aware it is SI-based and can experience serialization failures requiring retry). MySQL/InnoDB's default is REPEATABLE READ but, unusually, implements true phantom prevention via next-key (gap) locking, making it stronger than the SQL-92 minimum for that name. Distributed and cloud-scale databases (Google Spanner, CockroachDB, Amazon Aurora) build their transaction managers around variants of these same ideas, often defaulting to serializable-by-design using global timestamp ordering, because the write-skew and phantom pitfalls above are magnified across geographically distributed replicas. Financial systems, inventory-management systems, and any workload with cross-row invariants (double-entry accounting, seat/ticket allocation, referential constraints enforced at the application layer) are the classic domains where choosing too weak an isolation level produces subtle, hard-to-reproduce corruption — precisely the kind of failure that then requires careful analysis of the write-ahead log during Log-Based Crash Recovery to even diagnose after the fact.

Ask the tutor: Can you walk through why Serializable Snapshot Isolation only needs to detect cycles of rw-antidependencies, rather than aborting on every single rw-conflict?

Is REPEATABLE READ the same thing in every database?

No. The SQL-92 standard only requires REPEATABLE READ to prevent dirty reads and non-repeatable reads — phantoms are explicitly still allowed at this level. MySQL's InnoDB engine, however, implements REPEATABLE READ using next-key (gap) locking, which happens to also prevent phantom reads for many common query patterns, making it effectively stronger than the standard requires. PostgreSQL's REPEATABLE READ is implemented via snapshot isolation, which also prevents anomaly-table phantoms but can still permit write skew. Always check the specific engine's documentation; the SQL name is a floor, not a fixed specification.

Does snapshot isolation mean I never need to worry about concurrency bugs?

No. Snapshot isolation eliminates dirty reads, non-repeatable reads, and phantom reads as defined by the SQL-92 anomaly table, which covers a lot of common bugs cheaply and without blocking readers. But it does not achieve true serializability: write skew, where two transactions read overlapping data and write to disjoint items based on stale information, can silently violate multi-row invariants even though neither transaction's writes directly conflict. If your application has any invariant spanning more than one row, you need either explicit locking (`SELECT ... FOR UPDATE`), true serializable isolation (SSI), or careful redesign of the transactions to make their write sets overlap.

Why doesn't every system just default to SERIALIZABLE and avoid all these anomalies?

Because serializable execution has a real cost: lock-based implementations require predicate or gap locks that block writers on entire ranges, not just individual rows, and MVCC-based SSI implementations increase the abort-and-retry rate under contention since more transaction pairs get flagged as conflicting. For many workloads — a dashboard reading approximate counts, an audit log appending rows — the weaker anomalies simply never manifest in a way that matters, so the throughput cost of full serializability isn't justified. Isolation level selection is meant to be a deliberate per-workload decision, not a one-size-fits-all default.

How does READ COMMITTED actually prevent dirty reads while still being cheap?

In a locking implementation, READ COMMITTED takes a shared read lock immediately before reading a row and releases it immediately after — long enough to guarantee the row isn't mid-write by another transaction (since a writer's exclusive lock would conflict), but short enough that later reads within the same transaction take a fresh lock and can see newly committed changes. In an MVCC implementation, each individual statement is given a fresh snapshot of "the latest committed data as of now," so a statement can never see an in-flight, uncommitted write, but a later statement in the same transaction is free to see different (newer) committed data. Both mechanisms achieve the same anomaly guarantee via very different means.

What is the difference between a non-repeatable read and a phantom read?

A non-repeatable read concerns re-reading the exact same row and getting a different value — the set of rows involved is fixed, only the content of an existing row changed. A phantom read concerns re-running the same predicate-based query and getting a different set of rows — new rows appeared or existing ones vanished from the result. This distinction matters mechanically: preventing non-repeatable reads only requires locking (or versioning) specific existing rows, while preventing phantoms requires locking the predicate or range itself, since a phantom row didn't exist at the time of the first read and so could never have been locked by row identity alone.

Can an isolation level guarantee correctness without the application doing anything else?

Only SERIALIZABLE, correctly implemented (either full predicate-locking 2PL or SSI), guarantees that any concurrent execution is equivalent to some serial order, which is the strongest correctness guarantee a database offers on its own. At any weaker level, the application must reason about which anomalies are tolerable for its specific invariants, and often must add explicit safeguards — such as `SELECT ... FOR UPDATE`, optimistic version checks, or application-level retries — to compensate for anomalies the chosen isolation level permits. Isolation level is a foundation, not a substitute for careful transaction design.

How does this connect to crash recovery?

Isolation levels govern what concurrent transactions may observe about each other while all are running; crash recovery governs what must be true about committed data after a failure. Both rely on the same core notion of a transaction's atomic, all-or-nothing effect: a system implementing snapshot isolation must still write a durable, ordered log of committed versions so that after a crash it can reconstruct exactly which snapshots were valid and which in-flight writes must be undone. The version chains and commit-timestamp bookkeeping used to implement MVCC-based isolation levels are, in practice, deeply intertwined with the write-ahead logging machinery used for Log-Based Crash Recovery.

Before this

Where this leads