MVCC & Isolation Levels#
Problem statement (interviewer prompt)
Explain MVCC and the ANSI isolation levels (Read Uncommitted → Serializable). For a banking app, pick the right isolation level for transfers, statement reads, and a daily reconciliation job, and explain the anomalies each level prevents.
flowchart LR
T1[Txn 1<br/>reads snapshot]
T2[Txn 2<br/>writes new version]
R[(Row v1, v2, v3 ...<br/>versioned by tx id)]
T1 --> R
T2 --> R
R -. GC old versions .-> GC[Vacuum / GC]
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class T1,T2 service;
class R datastore;
Each transaction sees a consistent snapshot; writers don't block readers.
flowchart TB
subgraph MVCC[Multi-Version Concurrency Control]
direction TB
XID[Tx id / commit seqno]
SS[Snapshot = set of visible XIDs]
ROW[(Row chain<br/>v1 → v2 → v3)]
HOTUP[HOT updates / undo log]
GC[VACUUM / GC<br/>old tuples removed]
end
subgraph Levels[Isolation Levels - ANSI + extensions]
direction TB
RU[Read Uncommitted<br/>allows dirty reads]
RC[Read Committed<br/>no dirty reads]
RR[Repeatable Read<br/>same row -> same value]
SI[Snapshot Isolation<br/>own snapshot, write skew possible]
SR([Serializable<br/>equivalent to serial schedule])
SSI[Serializable Snapshot Isolation<br/>SI + detect dangerous structures]
end
subgraph Anomalies[Anomalies]
DR[Dirty read]
NRR[Non-repeatable read]
PR[Phantom read]
LU[Lost update]
WS[Write skew]
CR[Cycle / serializability]
end
subgraph Mechanisms[Mechanisms]
LOCK[2PL pessimistic locks]
OCC[OCC: validate at commit]
TS[Timestamp ordering]
LK[Predicate locks]
end
subgraph Engines
PG[Postgres<br/>MVCC undo via dead tuples + VACUUM]
INNO[InnoDB<br/>MVCC via undo log]
SPN[Spanner / CRDB<br/>TS via TrueTime / HLC]
ORA[Oracle<br/>MVCC via UNDO segments]
end
RU -. fixes DR .-> RC
RC -. fixes NRR .-> RR
RR -. fixes PR / WS .-> SR
SR -. impl as SI+detect .-> SSI
MVCC --- Engines
Mechanisms --- Levels
Anomalies --- Levels
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class XID,SS,HOTUP,GC,RU,RC,SI,SSI,DR,PR,LU,WS,CR,LOCK,OCC,TS,LK,INNO,ORA service;
class ROW,RR,NRR,PG,SPN datastore;
class SR compute;
Anomalies matrix (ANSI + Berenson)#
| Level | Dirty read | Non-rep read | Phantom | Write skew |
|---|---|---|---|---|
| RU | ✗ | ✗ | ✗ | ✗ |
| RC | ✓ | ✗ | ✗ | ✗ |
| RR | ✓ | ✓ | depends | ✗ |
| SI | ✓ | ✓ | ✓ for snapshot reads | ✗ |
| Serializable | ✓ | ✓ | ✓ | ✓ |
(✓ = prevented)
How MVCC works in Postgres#
- Each row tuple has
xmin(creating tx) andxmax(deleting tx). - A snapshot is
{ xmin, xmax, xip[] }- visible ifxmincommitted andxmaxnot committed/in snapshot. - Updates write a new tuple; old becomes dead → VACUUM removes.
- Long-running tx → bloat → autovacuum cannot reclaim → table grows.
Choosing a level#
- Most apps: Read Committed is fine.
- Bank transfer / inventory: Serializable or SSI.
- Reports: Repeatable Read (consistent snapshot).
- Beware lost updates in RC - use
SELECT ... FOR UPDATEor compare-and-set.
Optimistic vs pessimistic#
- OCC: validate at commit (Postgres SSI, CRDB). Great for low contention.
- 2PL: lock rows. Predictable but deadlocks. MySQL default.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
LSM vs B-Tree engines | WAL, memtable, SSTables, compaction | storage-engines-lsm-btree |
HLD |
MVCC & isolation levels | snapshot isolation, serializability, vacuum | mvcc-isolation-levels |
HLD |
Logical clocks | Lamport, vector clocks, HLC, TrueTime | logical-clocks |
Quick reference#
Why MVCC#
- Readers don't block writers; writers don't block readers.
- Enables consistent snapshots for backups, analytics, long reads.
Storage cost#
- Each update creates a new tuple → bloat.
- Postgres VACUUM, InnoDB undo log purge, Oracle UNDO tablespace.
- Long open transactions block GC.
Write skew (the classic SI gotcha)#
- Two surgeons on call; each checks the other is on call (true), then both go off.
- Both txns see the same snapshot, both commit, system ends up with zero on call.
- Fix: Serializable / SSI / explicit predicate lock / SELECT FOR UPDATE on both rows.
Cross-DB semantics#
| DB | Default isolation |
|---|---|
| Postgres | Read Committed (SI on request) |
| MySQL InnoDB | Repeatable Read (non-strict) |
| Oracle | Read Committed (SI on request) |
| SQL Server | Read Committed (snapshot opt) |
| CockroachDB | Serializable (always) |
| Spanner | External consistency (Serializable) |
API discipline#
- Wrap business invariants in DB constraints (FKs, CHECK, UNIQUE).
- Use
SELECT ... FOR UPDATEfor read-then-write under RC. - Retry on serialization failure (40001) - every modern driver supports this.
Refs#
- Berenson et al.: "A Critique of ANSI SQL Isolation Levels" (SIGMOD '95).
- Postgres concurrency control chapter.
- CockroachDB engineering blog on SSI.
- Jepsen analyses (PostgreSQL, MySQL, MongoDB) on jepsen.io.
FAQ#
What is MVCC?#
Multi-version concurrency control keeps multiple versions of each row. Readers see a consistent snapshot and never block writers, while writers create new versions atomically.
Read Committed vs Repeatable Read?#
Read Committed sees each statement's own snapshot. Repeatable Read pins the snapshot at transaction start, so the same query returns the same rows every time inside the txn.
What anomalies does Serializable prevent?#
Serializable prevents dirty reads, non-repeatable reads, phantoms, and write skew. The cost is more aborts or wider locks compared to Snapshot or Repeatable Read.
What is write skew?#
Two transactions each read a value, see it is safe, and write based on that read. Snapshot isolation allows it; Serializable Snapshot Isolation or explicit locks prevent it.
Which isolation level should I pick?#
Default to Read Committed for OLTP. Pick Repeatable Read or Snapshot for analytical reads. Use Serializable when correctness matters more than throughput, like for ledger transfers.
Related Topics#
- Database Sharding: MVCC isolation must be maintained across shards in distributed databases
- Distributed Transactions: distributed transactions must coordinate MVCC snapshots across nodes to provide serializable isolation
- CAP and PACELC Theorems: isolation levels directly map to the consistency guarantees a system offers within the CAP/PACELC framework
Further reading#
Curated, high-credibility sources for going deeper on this topic.