Break
Transactions, recovery, and the whole manager
Nano quiz ↗
press b or esc to carry on
M4

Transactions Motivation

A transaction is a multi-step database operation that must run as if it were a single atomic unit, even when thousands of other transactions are running on the same data simultaneously.

5 figures
motivation · Case Study: Taylor Swift Concert Sales on Ticketmaster

One Buy click is one transaction: three steps, all or none

One Buy click is one transaction: three steps, all or none Alice clicks Buy. That single click is one transaction wrapping three steps: check the seat is open, charge the card, record the sale. The transaction is atomic, so either all three commit together or none of them do, never a charge without a seat. This runs about ten thousand times per second with every fan reaching for the same seats. Color key: violet is the transaction and its commit, red is the all-or-nothing abort. One Buy click is one transaction Three steps bundled into a single all-or-nothing unit of work. Alice clicks Buy One transaction 1. check the seat is open 2. charge the card 3. record the sale Commit: all three happen seat booked, card charged, sale logged Abort: none happen never a charge without a seat × ~10,000 per second every fan reaching for the same seats at the same instant violet = the transaction & its commit  ·  red = the all-or-nothing abort
One fan's Buy click is a single transaction: check the seat is open, charge the card, record the sale, bundled so either all three commit or none do, never a charge without a seat and never the same seat to two fans. Multiply by the on-sale crush, every fan racing for the same seats, and you have the problem this module solves.
Notes ↗
motivation · Case Study: Taylor Swift Concert Sales on Ticketmaster

The Taylor Swift on-sale, by the numbers

The Taylor Swift on-sale, by the numbers Four figures from the on-sale: 52 show dates, 3.5 million fans pre-registered, 2.4 million tickets sold in a single day, and 3.5 billion system requests in a few hours. THE ON-SALE, BY THE NUMBERS 52 show dates 3.5M fans pre-registered 2.4M tickets sold in a single day 3.5B system requests in a few hours
The real failure was load, not correctness. About 12 million visitors and scalper bots drove 3.5 billion requests, four times the prior peak (violet). Under it, availability bent: queues froze thousands deep, Ticketmaster throttled and paused sales to stay up, and seat holds and payments timed out (red). But the transactions held: no seat sold twice, bots got nothing, and a record 2.4 million tickets sold in a day (green). Keeping that correctness while also staying fast under the crush is the problem this module solves.
Notes ↗
motivation · Case Study: Taylor Swift Concert Sales on Ticketmaster

What actually overwhelmed Ticketmaster: load, not double-booking

What actually overwhelmed Ticketmaster: load, not double-booking Three panels. The demand: about 12 million visitors plus scalper bots generated 3.5 billion system requests, four times the site's prior peak. What bent under it: virtual queues froze thousands deep, Ticketmaster deliberately slowed and paused sales to stay up, and seat holds and payments timed out under the contention. What held: no seat was sold twice, the bots acquired zero tickets, and 2.4 million tickets sold in one day, a record. The transactions stayed correct; availability and latency did not. Color key: violet is the demand, red is what buckled under load, green is what the transactions kept correct. What Actually Overwhelmed Ticketmaster Not double-booking. The transactions held. The load did not. The demand 12M visitors, plus scalper bots 3.5B system requests in hours 4× the prior peak more than the site was built for What bent: availability Queues froze, 2,000+ deep waiting rooms stalled for hours Sales slowed & paused throttled on purpose, to stay up Holds & payments timed out seat locks contended under load What held: correctness No seat sold twice the transactions did their job Bots got zero tickets they added load, not theft 2.4M sold in a day a single-day record Correctness survived. Delivering it fast, under that load, is the hard part, and the rest of this module. Color key  violet = the demand  ·  red = what buckled under load  ·  green = what stayed correct
The real failure was load, not correctness. About 12 million visitors and scalper bots drove 3.5 billion requests, four times the prior peak (violet). Under it, availability bent: queues froze thousands deep, Ticketmaster throttled and paused sales to stay up, and seat holds and payments timed out (red). But the transactions held: no seat sold twice, bots got nothing, and a record 2.4 million tickets sold in a day (green). Keeping that correctness while also staying fast under the crush is the problem this module solves.
Notes ↗
motivation · Case Study: Taylor Swift Concert Sales on Ticketmaster

Size was never the problem: Visa and Amazon clear far more, reliably

Size was never the problem: Visa and Amazon clear far more, reliably Three systems compared by peak throughput and whether they held. Visa clears about 100,000 transactions per second every day and holds. Amazon on Prime Day serves about 105 million requests per second under ten milliseconds and holds. Ticketmaster peaked at four times its own prior record and fell over. The difference is decades of transaction discipline. Size was never the problem Others clear far more, every single day, without melting. The difference is transaction discipline. Visa network 100,000 transactions / sec at peak, every day held Amazon, Prime Day 105 million requests / sec, answered in under 10 ms held Ticketmaster, Taylor Swift on-sale peaked at just 4× its own prior record fell over
Other systems clear far more, every day, without melting. Visa settles 100,000 transactions a second at peak; Amazon answers 105 million requests a second on Prime Day in under ten milliseconds. Ticketmaster fell over at four times its own prior record. The lesson is not that transactions are slow. It is that absorbing a sudden, massive spike of concurrent load takes the kind of engineering Visa and Amazon built over decades, and a one-off flash on-sale had not.
Notes ↗
motivation · The 100× Scale

Each generation of the stack is about 100 times the last

Each generation of the stack is about 100 times the last Three generations side by side. Classic, the 1990s to 2000s: about a thousand transactions per second, on hard disks with one to four cores, using B-Trees. Times ten thousand to Modern, the 2000s to 2020s: about ten million per second, on solid-state drives with 128-plus cores, using LSM Trees. Times ten thousand again to Future, the 2030s on: millions of AI agents per second, on NVMe with ten-thousand-plus cores, using learned indexes. Concurrency, hardware, and data structures all jump together. Each generation, about 10,000× the last Concurrency, hardware, and the data structures underneath all jump together. Classic 1990s – 2000s · banks, Oracle CONCURRENCY ~1,000s / sec HARDWARE HDD · 1–4 cores · 1 GB DATA STRUCTURES B-Trees ×10⁴ Modern 2000s – 2020s · Gmail, Roblox CONCURRENCY ~10M / sec HARDWARE SSD · 128+ cores · 1 TB DATA STRUCTURES LSM Trees ×10⁴ Future 2030s on · AI agents, serverless CONCURRENCY millions of agents / sec HARDWARE NVMe · 10K+ cores · 1 PB DATA STRUCTURES Learned indexes
Each generation runs roughly 10,000 times the load of the last (about 100× per decade), and concurrency, hardware, and data structures all jump together. The classic era ran thousands of transactions a second on disks and B-Trees; the modern era runs tens of millions on solid-state and LSM Trees; the agent era stacks a third jump on top, and has to hold the same correctness guarantees the classic era did.
Notes ↗
M4

ACID Properties

ACID names four guarantees that turn a sequence of database writes into a transaction: Atomicity (all-or-nothing), Consistency (no broken invariants), Isolation (concurrent transactions don't see each other's partial work), and Du

3 figures
acid-properties · Example: Transactions in a Ticketmaster-like App

Alice's ticket purchase: three steps, all or nothing

Alice's ticket purchase: three steps, all or nothing Alice's purchase is one transaction wrapping three steps: check inventory, verify the card, complete the sale. Top row, the happy path: all three succeed and the transaction commits. Bottom row, a failure: the card declines at step two, so the whole transaction rolls back and nothing happened, no seats reserved against a declined card. Color key: green is a committed step, grey struck-through is a step undone by rollback, pink is the decline. Alice's purchase: three steps, all or nothing One transaction wraps the steps. Every step commits together, or every step rolls back. BEGIN Check inventory seats available ✓ Verify card authorized ✓ Complete sale recorded ✓ COMMIT Alice gets her tickets. BEGIN Check inventory undone Verify card ✗ declined Complete sale never runs ROLLBACK Nothing happened. Atomicity  all three steps commit as one unit, or a single failure rolls every one of them back.
Alice's purchase is one transaction around three steps: check inventory, verify the card, complete the sale. Atomicity binds them. All three commit as a unit, or a single failure rolls every one of them back, so a sale is never confirmed against a declined card or sold-out seats.
Notes ↗
acid-properties · ACID for Data Trust

ACID: the four guarantees, the rule and the intuition for each

ACID: the four guarantees, the rule and the intuition for each Four panels, one per ACID guarantee, each tagged with its coloured letter circle. Atomicity (blue A): all the writes commit together, or none do; a declined card means no ticket, no charge, no change. Consistency (green C): every commit leaves the rules satisfied; seats sold and revenue always move as one. Isolation (orange I): concurrent transactions never see each other's half-done work; Alice locks the last seat and Bob waits, so no double sale. Durability (violet D): once committed it survives any crash; the receipt is shown, the server dies, and the ticket is still there. ACID: four guarantees The rule, and what each one looks like when Alice buys a ticket. A Atomicity All the writes commit together, or none do. Card declines, so no ticket, no charge, no change. C Consistency Every commit leaves the rules satisfied. Seats sold and revenue always move as one. I Isolation Transactions don't see each other's half-work. Alice locks the last seat; Bob waits. No double-sale. D Durability Once committed, it survives any crash. Receipt shown, server dies, the ticket is still there.
The four guarantees, one color per letter (the same circle reused on every ACID figure in the module). Each is a rule plus what it looks like when Alice buys a ticket: a declined card rolls back cleanly (atomicity), seats and revenue move as one (consistency), Alice locks the last seat so Bob waits (isolation), and a confirmed ticket survives a crash (durability).
Notes ↗
acid-properties · Example 2: Banking Transfer Race Condition

Isolation prevents the lost update: $324 without it versus $318 with it

Isolation prevents the lost update: $324 without it versus $318 with it Two columns share the start state Alice $200, Bob $100, total $300. Left, red, without isolation: the transfer credits Bob first so Bob is already $200 while Alice is not yet debited and still $200, the interest job reads that half-done state and applies 6% to $400, the moving $100 counted in both accounts, and the books end at $324, six dollars from nothing. Right, green, with isolation: the interest job waits on the transfer's lock, reads the settled $300, and ends at the correct $318, exactly $18 of interest. Color key: red is the interleaved run that invents money, green is the locked run that stays correct, grey is the shared start state. Isolation prevents the lost update A $100 transfer and a 6% interest job race at month-end. Same start, two outcomes. Start Alice $200 · Bob $100 · total $300 Without isolation: interleaved The interest job reads a half-done transfer. Transfer credits Bob first: $100 → $200 Alice is not debited yet, still $200 Interest reads now: $200 + $200 = $400 the moving $100 is counted in both accounts Interest +6% on $400 → Alice $212, Bob $212 Transfer finishes, debits Alice: $212 → $112 Alice $112 + Bob $212 = $324  $6 created from nothing With isolation: locked The interest job waits for the transfer to finish. Transfer locks the rows and commits Alice $200 → $100, Bob $100 → $200 Interest reads settled: $100 + $200 = $300 no half-done state is ever visible Interest +6% on $300 → Alice $106, Bob $212 the interest ran only after the transfer Alice $106 + Bob $212 = $318  exactly $18, 6% of $300 Isolation makes the two appear sequential: the interest job runs as if it simply followed the transfer. Color key  red = the interleaved run that invents money  ·  green = the locked run that stays correct
Two transactions race at month-end from the same start state: Alice $200, Bob $100, total $300. On the left (red), the transfer credits Bob first, so Bob already shows $200 while Alice is not yet debited and still shows $200; the interest job reads that half-done state and pays 6% on $400, the moving $100 counted twice, and the books end at $324, with $6 created from nothing. On the right (green), the interest job waits on the transfer's lock, reads the settled $300, and ends at the correct $318, exactly $18. The deeper point: isolation makes concurrent transactions appear sequential, as if the interest job simply ran after the transfer, so total money is conserved.
Notes ↗
M4

Case Study 4.1: ACID at Stripe

Every Stripe charge is a transaction that touches several tables (payment, ledger, merchant balance, webhook queue) and must satisfy all four ACID properties, or real money goes missing.

2 figures
case-study-transactions · What a Single Charge Touches

One Stripe charge is four writes, wrapped atomically

One Stripe charge is four writes, wrapped atomically One Pay button on a Stripe checkout fans out to four writes: charge the card through Visa, write the double-entry ledger as two rows that sum to zero, bump the merchant's running balance, and queue a webhook. A bracket marks all four as one atomic unit: all four land together, or none do. Color key: violet is the single tap and the atomic boundary, grey is the four writes. One tap, four writes A single Pay click fans out into four writes. Stripe wraps them so they land together, or none do. Pay $9.99 Charge the card through Visa Write the ledger two rows, double entry, sum to zero Bump the merchant balance running total + $9.99 Queue the webhook tell the merchant to ship all four, or none Atomic across the row  the four writes are one transaction; a failure anywhere rolls back all of them.
One Pay click is four writes: charge the card, write the double-entry ledger (two rows that must sum to zero), bump the merchant's balance, and queue a webhook. Stripe wraps them as one transaction, so all four land together or none do. The double-entry detail is why the ledger can never silently drift.
Notes ↗
case-study-transactions · What Each Letter Buys You

Four ways a Stripe payment breaks, and the guarantee that stops each

Four ways a Stripe payment breaks, and the guarantee that stops each A two-by-two grid of failures. Top left, the charge that wasn't: the card is charged but the webhook never queues, which atomicity prevents. Top right, the ledger that drifts: the two rows stop summing to zero, which consistency prevents. Bottom left, the lost update: two concurrent charges on one balance both read and write it, so one update is lost, which isolation prevents. Bottom right, lost to a crash: a confirmed charge vanishes when the server dies, which durability prevents. Color key: the red bar marks the failure, and the coloured letter is the ACID guarantee that stops it. Four ways money goes missing Each failure mode, and the ACID guarantee that stops it. The definitions are on the ACID Properties page. The charge that wasn't Card charged. Webhook never queued. Money left, no order ships. A Atomicity all of it, or none The ledger that drifts $9.99 debited, $9.98 credited. The two rows stop summing to zero. C Consistency the books always balance The lost update Two charges hit one balance at once. Both read it, both write, one is lost. I Isolation serial order, nothing lost Lost to a crash Receipt shown, then the server dies five milliseconds later. D Durability survives the crash red bar = the failure  ·  the coloured letter = the ACID guarantee that stops it
Four ways a payment breaks, and the guarantee that stops each: a partial charge where the card is charged but the webhook never queues (atomicity), a ledger whose two rows stop summing to zero (consistency), two concurrent charges that race on one balance so one update is lost (isolation), and a confirmed charge lost when the server crashes (durability). The full definitions are on the ACID Properties page; here each letter is a real way Stripe loses money.
Notes ↗
M4

Sample Code: Running Transactions

Every multi-step database operation that needs ACID guarantees is wrapped in BEGIN and COMMIT.

1 figure
transaction-examples · The Three Keywords

The three transaction keywords: BEGIN, COMMIT, ROLLBACK

The three transaction keywords: BEGIN, COMMIT, ROLLBACK A transaction block opens with BEGIN, runs your statements, and ends one of two ways: COMMIT makes every change since BEGIN permanent and visible to others, or ROLLBACK undoes every change as if nothing happened (rolling a transaction back is also called aborting it; in Postgres, ABORT is the same command). Outside such a block, each statement runs in autocommit mode as its own one-statement transaction. Color key: violet is the transaction block, green is COMMIT, red is ROLLBACK. One block, three keywords BEGIN opens it, your statements run, then COMMIT or ROLLBACK ends it. BEGIN TRANSACTION marks the start UPDATE inventory ... INSERT INTO orders ... many statements, one logical operation COMMIT every change since BEGIN is permanent and visible to other transactions ROLLBACK every change undone, as if nothing happened ROLLBACK = abort the transaction (in Postgres, ABORT) Outside the block: autocommit each statement is its own one-statement transaction violet = the block  ·  green = COMMIT  ·  red = ROLLBACK
The three keywords frame one block. BEGIN opens the transaction; your statements run inside as one logical operation; COMMIT makes every change since BEGIN permanent, or ROLLBACK undoes them all as if nothing happened. Rolling a transaction back is also called aborting it, the term the rest of this module uses (in Postgres, ABORT is the same command). Outside a block, each statement autocommits as its own one-statement transaction.
Notes ↗
M4

Naive Concurrency

A database runs many transactions at once, and the hard part is coordinating how they touch shared rows.

4 figures
naive-concurrency · Naive way 1: run them one at a time

I/O lag: Read-start (Rs) at time 6 and Read-end (Re) at time 11 on resource B, with a ~5 unit gap of CPU idle time between them

I/O lag: Read-start (Rs) at time 6 and Read-end (Re) at time 11 on resource B, with a ~5 unit gap of CPU idle time between them A short time axis for one disk read on resource B. A violet marker at time 6 is the read-start (Rs); a second violet marker at time 11 is the read-end (Re). Between them a dashed span labels roughly five units of CPU idle time, the gap another transaction can use. Color key: violet marks the read-start and read-end events. 6 7 8 9 10 11 Rs B Re B ~5 units of CPU idle
One disk read (the dashed span between the two violet markers, its start and end) stalls the transaction far longer than the work around it. Under serial execution, nothing runs in that gap.
Notes ↗
naive-concurrency · Naive way 1: run them one at a time

Run them one at a time: every wait is wasted

Run them one at a time: every wait is wasted A timeline of one server running transactions serially, left to right. Violet blocks are transactions actually computing; wide grey blocks are the server sitting idle while a transaction waits on a disk read. Because only one transaction runs at a time, every grey gap is wasted: no other transaction may use it. A long-running transaction, T2, stretches its idle span so the transactions queued behind it, T3 and T4, wait far longer than their own work needs. Color key: violet is a transaction running, grey is the server idle on disk while everyone else is blocked. Run them one at a time: every wait is wasted One server, one transaction start to finish. While it waits on disk, the whole queue waits too. SERVER time → T1 run idle disk read T2 run idle long disk read, nobody else runs T3 run T4 … T3 and T4 sit blocked behind T2's wait, far longer than their own work needs. Color key  violet = a transaction running  ·  grey = the server idle on disk while everyone else is blocked
Serial execution wastes every wait. The violet blocks are transactions computing; the grey blocks are the server idle on disk. Because no one else may run in a gap, a long transaction (T2) stalls the whole queue: T3 and T4 wait far longer than their own work needs. The Ticketmaster on-sale would serialize into one slow line, even across unrelated rows.
Notes ↗
naive-concurrency · Naive way 2: let them all run

Run them free: three ways the same data goes wrong

Run them free: three ways the same data goes wrong Three side-by-side failure panels, each a tiny two-column schedule of T1 and T2 reading and writing one row with no coordination, time running top to bottom. Left, the lost update: both read 100, both write 101, so the count lands on 101 instead of 102. Middle, the dirty read: T1 writes 150, T2 reads it, then T1 aborts, so T2 acted on a value that never committed. Right, the stale read: T1 reads 20, T2 writes 30 and commits, T1 reads again and sees 30, disagreeing with itself inside one transaction. Each panel names its conflict type: write-write, write-read, read-write. Color key: red is the corrupted result each race produces, grey is the trace. Run them free: three ways the same data goes wrong No coordination is fast, but two transactions on one row leave a result no one-at-a-time order could. Lost update write – write conflict play_count = 100 T1 T2 read → 100 read → 100 write 101 write 101 lands on 101, not 102 one play silently vanishes Dirty read write – read conflict balance = 100 T1 T2 write 150 read → 150 ABORT acts on 150 T2 used 150 a value that never committed Stale read read – write conflict price = 20 T1 T2 read → 20 write 30 commit read → 30 T1 saw 20, then 30 two reads disagree in one txn Color key  red = the corrupted result each race leaves  ·  grey = the trace and the conflict type
Uncontrolled concurrency corrupts one row three different ways. In a lost update, two transactions both read 100 and both write 101, so one play disappears. In a dirty read, one transaction reads another's uncommitted write and the writer then aborts, so the reader acted on a value that never existed. In a stale read, a transaction reads the same row twice and a committed write in between makes its two reads disagree.
Notes ↗
naive-concurrency · The middle: concurrency control

The scheduling spectrum: safety on the left, performance on the right, the sweet spot in the middle

The scheduling spectrum: safety on the left, performance on the right, the sweet spot in the middle A horizontal axis runs from Maximum Safety and slow on the left to Maximum Performance and broken on the right. Three points sit on it. SERIAL on the left, labelled Naive 1, runs one transaction at a time: correct, but no parallelism. NO COORDINATION on the right, labelled Naive 2, runs everything at once: maximum speed, but races corrupt the data. In the middle, violet-highlighted, CONCURRENCY CONTROL interleaves only the safe swaps: serial-correct at concurrent speed. Color key: violet is the sweet spot, the chosen point; green is the correct end; red is the broken end; grey is the axis. The scheduling spectrum Run them one at a time and it is correct but slow. Run them with no rules and it is fast but corrupt. MAXIMUM SAFETY slow, no parallelism MAXIMUM PERFORMANCE fast, data corruption SERIAL Naive 1 One transaction at a time. Always correct. No parallelism, no throughput. NO COORDINATION Naive 2 Everything runs at once. Maximum speed. Races corrupt the data. CONCURRENCY CONTROL the sweet spot Interleave only the safe swaps. Serial-correct at concurrent speed. Rules 1 and 2 decide what may swap. Color key  violet = the sweet spot  ·  green = the correct end  ·  red = the broken end  ·  grey = the axis
Concurrency control sits between the two naive ends, overlapping only the actions whose order does not matter. Serial (Naive 1), on the left, is correct but has no throughput; free-for-all (Naive 2), on the right, has throughput but corrupts data. The rest of this section makes "order does not matter" precise, then builds the engines that deliver it.
Notes ↗
M4

A Quick Graph Refresher

The correctness test ahead builds a directed graph from a schedule and checks it for a cycle.

2 figures
graph-refresher · Cyclic vs Acyclic Graphs

Side-by-side: an acyclic 3-node graph T1→T2→T3 vs a cyclic 3-node graph T1→T2→T3→T1

Side-by-side: an acyclic 3-node graph T1→T2→T3 vs a cyclic 3-node graph T1→T2→T3→T1 Side-by-side: an acyclic 3-node graph T1→T2→T3 vs a cyclic 3-node graph T1→T2→T3→T1 Acyclic (DAG) T1 T2 T3 T1 → T2 → T3 Cyclic (not a DAG) T1 T2 T3 T1 → T2 → T3 → T1
On the left, an acyclic graph: follow the arrows and you never return to a node, so the nodes line up in order. On the right, a cyclic graph: the loop means no consistent order exists. On the next page, a cycle in a schedule's graph marks the schedule unsafe.
Notes ↗
graph-refresher · Topological Sort

A 4-node DAG and its two valid topological orderings, listed as text

A 4-node DAG and its two valid topological orderings, listed as text A 4-node DAG on the left (edges A to B, A to C, B to D, C to D) and, on the right, its two valid topological orderings listed as text: A B C D, and A C B D. B and C are independent, so either can come first. DAG A B C D Two valid orders A → B → C → D A → C → B → D B and C are independent
The DAG has two valid topological orders, A → B → C → D and A → C → B → D; B and C are independent, so either can come first. Each is a safe sequence. On the next page, each valid topological order of a schedule's graph is an equivalent serial run.
Notes ↗
M4

Correctness: Ensuring Safe Concurrent Execution

The database reorders the actions of concurrent transactions to run them fast, but the outcome must still be correct: equivalent to running the transactions in *some* serial order.

12 figures
correctness · Key Definitions

Actions: a transaction's reads and writes

Actions: a transaction's reads and writes A transaction is a sequence of actions, each one a read or write of a single row, written like T1.R(A) for transaction T1 reading row A. T1 reads A, writes A, then reads B. Blue boxes are reads, orange boxes are writes. Correctness tracks only the order of these actions, not the values they compute. Actions: a transaction's reads and writes Each action reads or writes one row. A transaction is a sequence of them. read write T1.R(A) T1.W(A) T1.R(B) Correctness tracks only the order of these actions, not the values they compute (A-=1, B*=10).
A transaction is a sequence of actions, each one a read or write of a single row, written T1.R(A) for transaction T1 reading row A. Blue boxes are reads, orange boxes are writes. Correctness tracks only their order, not the values they compute.
Notes ↗
correctness · Key Definitions

Serial, interleaved, and serializable schedules

Serial, interleaved, and serializable schedules Three kinds of schedule for two transactions T1 and T2, each box one action written like T1.R(A). A serial schedule runs T1's actions to completion then T2's. An interleaved schedule mixes them in time. A serializable schedule is an interleaving whose result matches some serial order. Blue boxes are reads, orange boxes are writes, and the T1 or T2 prefix names the transaction. Color key: blue read, orange write. Three kinds of schedule Serial One transaction runs to completion, then the next. No overlap. T1.R(A) T1.W(A) T2.R(B) T2.W(B) Interleaved Actions from different transactions mixed in time. T1.R(A) T2.R(B) T1.W(A) T2.W(B) Serializable An interleaving whose result matches SOME serial order. T1.R(A) T2.R(B) T1.W(A) T2.W(B) T1.R(A) T1.W(A) T2.R(B) T2.W(B) a serial run Blue box = read, orange box = write. The T1 or T2 prefix names the transaction, written R(A) for read row A, W(A) for write row A. This page tracks only the R/W order, the macro view. Timing, IO, and locks come later, on The Lock Manager page.
Serial runs one transaction's actions to completion, then the next. Interleaved mixes them in time. Serializable is an interleaving whose result matches some serial order. Blue boxes are reads, orange writes; the T1 or T2 prefix names the transaction, and the page tracks only the R/W order, the macro view.
Notes ↗
correctness · Conflicts: The Heart of Concurrency Control

2 by 2 matrix of action pairs: only R-R is a non-conflict; the other three (R-W, W-R, W-W) are all conflicts

2 by 2 matrix of action pairs: only R-R is a non-conflict; the other three (R-W, W-R, W-W) are all conflicts A 2 by 2 matrix comparing one action of transaction T1 (rows) against one action of transaction T2 (columns), both on the same row. Only read-read commutes (green, no conflict); read-write, write-read, and write-write are conflicts (red). A conflict needs two transactions, the same row, and at least one write. T₂ Reads T₂ Writes T₁ Reads T₁ Writes R–R commute · no conflict R–W conflict (stale read) W–R conflict (dirty read) W–W conflict (lost update) Two transactions, the same row, at least one a write.
A conflict needs two transactions, the same row, and at least one write. Only read-read commutes; the other three each leave an order-dependent result, each a named anomaly: read-write a stale read, write-read a dirty read, write-write a lost update. Every conflict becomes a directed edge in the conflict graph, next.
Notes ↗
correctness · How to Test for Conflict-Serializability

The conflict-serializability test: build a conflict graph, then check for a cycle

The conflict-serializability test: build a conflict graph, then check for a cycle The test in three steps. First, one node per transaction (T1, T2, T3 as violet circles). Second, a directed edge for every conflict, here forming the acyclic chain T3 to T1 to T2. Third, look for a cycle: the two outcomes are shown as a green box, no cycle means conflict-serializable because a serial order respects every edge, and a red box, a cycle means not conflict-serializable because two edges demand opposite orders. Color key: violet is the graph, green is the safe outcome, red is the unsafe outcome. The conflict-serializability test Build a graph from the conflicts, then look for a cycle. 1 · ONE NODE PER TRANSACTION T1 T3 T2 2 · each conflict is a directed edge 3 · LOOK FOR A CYCLE ✓ No cycle → conflict-serializable A topological order respects every edge, so it is a safe serial run. ✗ A cycle → not conflict-serializable Two edges demand opposite orders, so no serial order can satisfy them. Color key  violet = the graph  ·  green = no cycle, safe  ·  red = a cycle, unsafe
The whole test in three steps. Build one node per transaction, add a directed edge for every conflict, then look for a cycle. No cycle (green) means a serial order respects every edge, so the schedule is conflict-serializable; a cycle (red) means two edges demand opposite orders, so none does.
Notes ↗
correctness · Example 1: Concert Ticket Booking

A serial schedule: T1, then T2, then T3, with no overlap

A serial schedule: T1, then T2, then T3, with no overlap A MacroSchedule shown as a table of Transaction, Action, Variable, Step, and Value, one row per operation in step order. Consecutive actions of one transaction are bracketed as a block on the left, so a serial schedule reads as one block per transaction. Color key: blue is a read, orange is a write. Start values: { A = 10, B = 22 } MacroSchedule.Run() Transaction Action Variable Step Value End values: { A = 9, B = 20 } T1 T2 T3 T1 R A 1 10 T1 W A 2 9 T2 R A 3 9 T2 R B 4 22 T2 W B 5 21 T3 R B 6 21 T3 W B 7 20
A serial schedule runs T1, then T2, then T3 with no overlap. The brackets on the left make the shape plain: each transaction's actions form one contiguous block, the visual signature of serial. It ends at A=9, B=20 with ACID intact, but with no parallelism it is the slowest of the three.
Notes ↗
correctness · Example 1: Concert Ticket Booking

Even a serial schedule has conflicts, and they chain T1 to T2 to T3

Even a serial schedule has conflicts, and they chain T1 to T2 to T3 Conflict-serializability analysis: a table of conflicting actions (Txn 1, Txn 2, Variable, and the step in each), then a conflict graph with one node per transaction and a directed edge per conflict, and a verdict. A cycle (drawn in red) means the schedule is not conflict-serializable; an acyclic graph is serializable and equivalent to the serial order shown. Conflicting Actions Txn 1 Txn 2 Variable Step (Txn 1) Step (Txn 2) Conflict Graph T1 T2 A 2 3 T2 T3 B 4 7 T2 T3 B 5 6 T2 T3 B 5 7 T1 T2 T3 T1 T2 A 2 3 A-(#2,#3) T2 T3 B 4 7 T2 T3 B 5 6 T2 T3 B 5 7 B-(#4,#7) B-(#5,#6) B-(#5,#7) Acyclic → conflict-serializable. Equivalent serial order: T1 → T2 → T3 Equivalent Serial Schedule Transaction Action Variable Step T1 R A 1 T1 W A 2 T2 R A 3 T2 R B 4 T2 W B 5 T3 R B 6 T3 W B 7
Even a serial schedule has conflicts wherever transactions touch a shared variable: a W-R on A (T1 → T2), a W-W on B (T2 → T3), four in all. They chain T1 → T2 → T3, which is exactly the serial order that produced them.
Notes ↗
correctness · Example 2: Interleaved Schedule Analysis

S1': the same three transactions, interleaved

S1': the same three transactions, interleaved A MacroSchedule shown as a table of Transaction, Action, Variable, Step, and Value, one row per operation in step order. Consecutive actions of one transaction are bracketed as a block on the left, so a serial schedule reads as one block per transaction. Color key: blue is a read, orange is a write. Start values: { A = 10, B = 22 } MacroSchedule.Run() Transaction Action Variable Step Value End values: { A = 9, B = 20 } T1 R A 1 10 T2 R A 2 10 T1 W A 3 9 T2 R B 4 22 T2 W B 5 21 T3 R B 6 21 T3 W B 7 20
S1' interleaves T1, T2, and T3 instead of running them serially. Whether the interleaving stays correct depends entirely on its conflict graph, built next.
Notes ↗
correctness · Example 2: Interleaved Schedule Analysis

S1' has no cycle in its conflict graph, so the interleaving is safe

S1' has no cycle in its conflict graph, so the interleaving is safe Conflict-serializability analysis: a table of conflicting actions (Txn 1, Txn 2, Variable, and the step in each), then a conflict graph with one node per transaction and a directed edge per conflict, and a verdict. A cycle (drawn in red) means the schedule is not conflict-serializable; an acyclic graph is serializable and equivalent to the serial order shown. Conflicting Actions Txn 1 Txn 2 Variable Step (Txn 1) Step (Txn 2) Conflict Graph T2 T1 A 2 3 T2 T3 B 4 7 T2 T3 B 5 6 T2 T3 B 5 7 T1 T2 T3 T2 T1 A 2 3 A-(#2,#3) T2 T3 B 4 7 T2 T3 B 5 6 T2 T3 B 5 7 B-(#4,#7) B-(#5,#6) B-(#5,#7) Acyclic → conflict-serializable. Equivalent serial order: T2 → T1 → T3 Equivalent Serial Schedule Transaction Action Variable Step T2 R A 1 T2 R B 2 T2 W B 3 T1 R A 4 T1 W A 5 T3 R B 6 T3 W B 7
The conflict graph of S1' has four conflicts and no cycle, so S1' is conflict-serializable: it matches the serial order T2 → T1 → T3 (or T2 → T3 → T1). The faster interleaving is still correct.
Notes ↗
correctness · Example 2: Interleaved Schedule Analysis

S2': a different interleaving, with an extra T2 write on B

S2': a different interleaving, with an extra T2 write on B A MacroSchedule shown as a table of Transaction, Action, Variable, Step, and Value, one row per operation in step order. Consecutive actions of one transaction are bracketed as a block on the left, so a serial schedule reads as one block per transaction. Color key: blue is a read, orange is a write. Start values: { A = 10, B = 22 } MacroSchedule.Run() Transaction Action Variable Step Value End values: { A = 9, B = 20 } T1 R A 1 10 T2 R A 2 10 T1 W A 3 9 T2 R B 4 22 T2 W B 5 21 T3 R B 6 21 T3 W B 7 20 T2 W B 9 20
S2' is a different interleaving of the same three transactions. The extra T2 write on B breaks it; the conflict graph next shows how.
Notes ↗
correctness · Example 2: Interleaved Schedule Analysis

S2' has a cycle in its conflict graph, so the interleaving is unsafe

S2' has a cycle in its conflict graph, so the interleaving is unsafe Conflict-serializability analysis: a table of conflicting actions (Txn 1, Txn 2, Variable, and the step in each), then a conflict graph with one node per transaction and a directed edge per conflict, and a verdict. A cycle (drawn in red) means the schedule is not conflict-serializable; an acyclic graph is serializable and equivalent to the serial order shown. Conflicting Actions Txn 1 Txn 2 Variable Step (Txn 1) Step (Txn 2) Conflict Graph T2 T1 A 2 3 T2 T3 B 4 7 T2 T3 B 5 6 T2 T3 B 5 7 T3 T2 B 6 9 T3 T2 B 7 9 T1 T2 T3 T2 T1 A 2 3 A-(#2,#3) T2 T3 B 4 7 T2 T3 B 5 6 T2 T3 B 5 7 B-(#4,#7) B-(#5,#6) B-(#5,#7) T3 T2 B 6 9 T3 T2 B 7 9 B-(#6,#9) B-(#7,#9) Cycle T3 → T2 → T3. So NOT conflict-serializable.
The conflict graph of S2' contains a cycle, so S2' is not conflict-serializable, and the interleaving is unsafe.
Notes ↗
correctness · Why the cycle test works

Sliding an interleaved schedule into a serial one by swapping non-conflicting neighbours

Sliding an interleaved schedule into a serial one by swapping non-conflicting neighbours An interleaved schedule of four actions across two transactions. T1's two tiles (read A, write B), highlighted in violet, start split apart with a T2 action between them. The goal is to swap non-conflicting neighbours to gather T1's actions together, and then T2's, into contiguous blocks, which is a serial schedule. Markers between tiles show which pairs may swap (violet) and the one write-write conflict on B that may not (red). One free swap of the middle pair gathers T1 into one block and T2 into the next, giving the serial order T1 then T2. Color key: violet marks T1 and the free swaps, red marks the conflict that cannot move. Can we swap this into a serial schedule? Gather each transaction's actions into one block by swapping free neighbours. Watch T1, in violet. T1 R(A) T2 R(A) T1 W(B) T2 W(B) R, R A ≠ B W, W on B one free swap T1 block T2 block T1 R(A) T1 W(B) T2 R(A) T2 W(B) serial: T1, then T2 the one conflict (W, W on B) is what fixed T1 before T2 Color key  violet = T1 and the free swaps  ·  red ✗ = the conflict you cannot cross
Can we swap an interleaved schedule into a serial one? The goal is to gather each transaction's actions into one contiguous block. T1's two tiles (violet) start apart, with a T2 action between them. A neighbour pair may swap only when it does not conflict; here the middle pair touches different rows, so one swap drops T1 into a block and T2 into the next, leaving the serial order T1 then T2. The write-write conflict on B (red) is the one pair that cannot move. A schedule untangles like this exactly when its conflict graph is acyclic.
Notes ↗
correctness · The Landscape of Schedule Classes

Nested hierarchy: every S2PL schedule is a 2PL schedule, every 2PL schedule is conflict-serializable, every conflict-serializable schedule is serializable

Nested hierarchy: every S2PL schedule is a 2PL schedule, every 2PL schedule is conflict-serializable, every conflict-serializable schedule is serializable Nested hierarchy: every S2PL schedule is a 2PL schedule, every 2PL schedule is conflict-serializable, every conflict-serializable schedule is serializable Serializable schedules Conflict-Serializable (CS) 2PL S2PL S2PL ⊂ 2PL ⊂ Conflict-Serializable ⊂ Serializable
Each ring is a set of schedules, strictly contained in the next: every conflict-serializable schedule is serializable, every two-phase-locking (2PL) schedule is conflict-serializable, and Strict-2PL sits inside 2PL. The gap between serializable and CS is a few rare blind-write schedules (a write that never reads its row), serializable yet not CS. The containment is also a difficulty gradient: the outer ring is checkable only by brute force, CS by the mechanical cycle test, and the 2PL rings, which the Two-Phase Locking page builds a few pages on, need no test at all, since the protocol enforces membership.
Notes ↗
M4

Locks Stack & the Lock Manager

Concurrent transactions are independent multi-step operations running at the same time on shared data, and the database makes each one act as if no other transaction exists.

3 figures
concurrency-stack

The concurrency full stack: one COMMIT, four layers down to one instruction

The concurrency full stack: one COMMIT, four layers down to one instruction A four-layer call stack. Your Code runs COMMIT and sees one transaction. It delegates down to the Database, whose lock manager acquires an exclusive lock on the row. The lock manager delegates down to the Operating System, which takes a mutex on its thread. The OS delegates down to the Hardware, which executes one compare-and-swap, the single indivisible instruction every layer reduces to. The guarantee then returns back up the right side: swapped atomically, mutex held, row locked, transaction isolated. Color key: violet is the call delegating down; grey is the layers and the guarantee returning up. The concurrency full stack One COMMIT delegates down four layers, until it bottoms out at a single atomic instruction. YOUR CODE You see one transaction. BEGIN; UPDATE Likes; COMMIT; DATABASE · LOCK MANAGER Decides WHAT to lock. lockManager.acquire(likesRow, X) OPERATING SYSTEM Knows HOW to lock. mutex_lock(&latch) // semaphore, spinlock HARDWARE · THE BASE One indivisible instruction. compare_and_swap(p, old, new) guarantee returns up Color key  violet = the call delegating down  ·  grey = the layers and the guarantee returning up
One COMMIT, four layers down to a single instruction. The lock manager decides what to lock, the OS mutex decides how, and the hardware's compare-and-swap is the one indivisible step everything reduces to. The guarantee returns up the right side: swapped atomically, mutex held, row locked, transaction isolated.
Notes ↗
concurrency-stack · Why OS vs Database Locks?

OS lock vs database lock manager: same word, different layer

OS lock vs database lock manager: same word, different layer A two-column comparison. The left column, grey, is the OS lock: a mutex or semaphore that is binary locked or unlocked, has no idea what data it guards, coordinates threads, and leaves deadlock for you to detect by hand. The right column, violet, is the database lock manager: it locks at table, page, or row granularity, supports intent locks that announce a plan, resolves deadlock automatically, and scales to millions of transactions. Color key: grey is the OS lock, a mutex or semaphore; violet is the database lock manager. Same word, two layers on one machine The database lock manager is a software layer that sits above the OS’s mutex, not a replacement for it. OS LOCK · MUTEX / SEMAPHORE One lock bit, for thread coordination. mutex_lock(&latch) // taken or free Binary scope One bit: locked or unlocked. No shared mode. No data awareness Guards a memory address. Knows nothing of rows. Thread-level Coordinates threads inside one process. Deadlock is your problem · detect by hand. DATABASE LOCK MANAGER A coordinator that understands the data. lockManager.acquire(likesRow, IX) Table → page → row granularity Lock the whole table or one row, as needed. Intent locks “I plan to write rows here.” Announce ahead. Transaction-level, million scale Coordinates millions of transactions at once. Deadlock resolved for you · detect, abort a victim. Color key  grey = the OS lock (mutex or semaphore)  ·  violet = the database lock manager
The same word, two layers on one machine. The OS lock (grey) is one bit of state guarding a memory address it knows nothing about. The database lock manager (violet) locks at table, page, or row granularity, lets a transaction announce intent, and resolves deadlock for millions of transactions at once. The OS primitive cannot do any of that, so the database builds its own lock layer on top.
Notes ↗
concurrency-stack · The lock manager

The lock manager: one component, two goals, three jobs

The lock manager: one component, two goals, three jobs Three transactions each send a lock request to one central component, the lock manager, which keeps a lock table of who holds each row and who is waiting. Its mission is two goals: keep every schedule correct (equal to some serial order) and fast (overlap the idle waits). It does three jobs. Job one, shown here in violet, is to grant a free row or queue you behind the holder. Jobs two and three are greyed as coming up: schedule many transactions, and break deadlocks. Color key: violet is the job this page covers, grey is a job still ahead, green marks the correctness goal. The lock manager: who waits, who runs Every transaction asks one component for its locks. Here is what that component owes, and what it does. Correct · equal to some serial order Fast · overlap the idle waits T1 wants row A T2 wants row A T3 wants row B lock requests LOCK MANAGER · THE LOCK TABLE Row A held by T1 · waiting: T2 Row B free → granted to T3 1 Grant or queue Row free? Hand out the lock. Row held? Queue you behind the holder. This page is this job. 2 Schedule many txns AHEAD Overlap waits so many transactions finish fast, not one at a time. 3 Break deadlocks AHEAD Two transactions each waiting on the other, forever? Abort one.
Every transaction asks one component, the lock manager, for its locks, and it keeps the lock table of who holds each row and who waits. It owes two goals: correct, equal to some serial order, and fast, overlapping the idle waits. Its first job is the one shown, granting a free row or queuing you behind the holder; scheduling many transactions and breaking deadlocks (greyed) each get their own page later.
Notes ↗
M4

DB Locks Lifecycle

A lock is a claim a transaction puts on a row before it reads or writes.

3 figures
lock-manager-cycle · One lock, start to finish

The lock lifecycle, in the schedule notation

The lock lifecycle, in the schedule notation One transaction's lock cycle on a time axis, drawn in the same notation the lock schedules ahead use. A green flag is a request (Req), a red flag is the grant (Get), a light-blue box is the read, a light-orange box is the write, and a purple flag pointing forward is the release (Unl). T1 requests seat A at time 1, gets it at 2, reads then writes while holding it, and unlocks at 8. T2 requests the same lock at time 2, waits the whole time T1 holds it, and gets it only after T1 unlocks. The lock lifecycle One transaction's lock on a timeline, in the same flags the schedules ahead use. Req Get read write Unl (release) T1 Req(A) 1 Get(A) 2 T1 holds A Rs–Re 4 Ws–We 6 Unl(A) 8 T2 Req(A) waits the whole time T1 holds A Get(A) now its turn time
One lock through its cycle, in the notation the schedules ahead use: a green flag is the request (Req), a red flag the grant (Get), a blue box the read, an orange box the write, and a purple flag the release (Unl). T1 takes row A, reads then writes while holding it, and releases. T2 wants the same row: it requests, waits the whole hold, and gets it only once T1 releases.
Notes ↗
lock-manager-cycle · Two kinds of lock: shared and exclusive

Shared and exclusive locks

Shared and exclusive locks Two lock modes on seat A15. On the left, a shared lock for reading: T1, T2, and T3 each take a shared lock and all read the status as available at the same time, because reads do not conflict. On the right, an exclusive lock for writing: T1 takes an exclusive lock and writes the status to sold, while T2 and T3 are blocked and must wait. Two kinds of lock Reads can share one row. A write needs the row to itself. SHARED (S) · FOR READING Checking seat A15 availability T1: Get(S) R(A15) = "available" T2: Get(S) R(A15) = "available" T3: Get(S) R(A15) = "available" All three hold S at once. Reads never conflict, so they share the row. EXCLUSIVE (X) · FOR WRITING Buying seat A15 T1: Get(X) W(A15) = "sold to T1" T2: [blocked] cannot read or buy T3: [blocked] cannot read or buy Only T1 holds X. T2 and T3 wait, so no one sees a half-written row.
A shared lock (S) is for reading: several transactions can hold S on one row and read it together, because reads do not conflict. An exclusive lock (X) is for writing: only one transaction holds X, and everyone else waits.
Notes ↗
lock-manager-cycle · Two kinds of lock: shared and exclusive

Lock compatibility matrix: shared and exclusive locks

Lock compatibility matrix: shared and exclusive locks A two-by-two matrix. Rows are the lock already held, columns the lock now requested. Holding a shared lock and requesting a shared lock is compatible, because two reads do not conflict. Every other combination, shared then exclusive, exclusive then shared, exclusive then exclusive, is incompatible, so the requester waits. A write excludes everyone. Lock compatibility Two reads share one row. A write excludes everyone. wants Shared (S) wants Exclusive (X) holds ↓ / wants → holds Shared (S) compatible, both read wait holds Exclusive (X) wait wait one green cell: two shared locks coexist. Any exclusive lock, held or wanted, forces a wait.
The one compatible pairing is shared-with-shared; every cell touching an exclusive lock forces a wait. This is the conflict rule from the Correctness page made operational: two operations collide only when they touch the same row and at least one writes.
Notes ↗
M4

Two-Phase Locking

A transaction holds many locks across its life, and Two-Phase Locking (2PL) is the discipline for using them: every transaction grows its lock set first and shrinks it second, never reacquiring.

3 figures
db-locks-2pl · Two-Phase Locking (2PL): Growing then Shrinking

2PL: locks held rise during the growing phase, peak, then fall during the shrinking phase

2PL: locks held rise during the growing phase, peak, then fall during the shrinking phase A lock-count curve over the life of one transaction. Locks held (violet) climb step by step through the growing phase, peak, then fall step by step through the shrinking phase. A dashed divider marks the first unlock, the moment the protocol crosses from growing to shrinking. Color key: violet is the locks-held curve. Two-Phase Locking (2PL) begin commit Number of Locks time → growing phase acquire only first unlock shrinking phase release only
2PL: the lock count only rises, then only falls. The peak is the moment the transaction holds everything it will ever need.
Notes ↗
db-locks-2pl · Two-Phase Locking (2PL): Growing then Shrinking

Strict 2PL (S2PL): locks held rise during the growing phase, plateau, then drop to zero in one step at COMMIT

Strict 2PL (S2PL): locks held rise during the growing phase, plateau, then drop to zero in one step at COMMIT A lock-count curve over the life of one transaction under Strict 2PL. Locks held (violet) climb step by step, plateau while the transaction holds every lock, then drop to zero in a single step at commit. A dashed divider marks the commit, the only point where locks release. Color key: violet is the locks-held curve. Strict 2PL (S2PL) begin commit Number of Locks time → growing → hold acquire, then keep all locks at commit release all at once
Strict 2PL: locks rise, plateau, then drop to zero in one step at commit. Nothing is released early.
Notes ↗
db-locks-2pl · The Cost of Releasing Early: Cascading Aborts

Why strict 2PL stops cascading aborts

Why strict 2PL stops cascading aborts Two protocols compared. Under regular 2PL, T1 writes x then unlocks it early, T2 reads that uncommitted value, and when T1 aborts, T2 has read a value that never committed and must abort too, a cascading abort. Under strict 2PL, T1 holds the lock on x until it commits or aborts, so T2 waits and only ever reads a committed value, and there is no cascade. Why strict 2PL: stop the cascade Releasing a lock early lets another transaction read work you might still undo. REGULAR 2PL · RELEASES EARLY T1: W(x) Unl(x) releases early T2: Get(x) R(x) reads uncommitted x T1: ABORT T2 read a value that never committed. T2 must abort too: a cascading abort. STRICT 2PL · HOLDS TO COMMIT T1: W(x) holds the lock T2: [waits] T1: ABORT Unl(x) T2: R(x) ok T2 only ever reads committed values. No one builds on work that gets undone. No cascade.
Release a lock early (regular 2PL) and another transaction can read work you later undo, so its abort cascades into theirs. Strict 2PL holds every lock to commit, so no one ever builds on uncommitted work. It is the sous-chef who hands over a sauce only once the head chef has approved it.
Notes ↗
M4

Microschedules & Deadlock

You met the lock manager and one lock's cycle; now it runs *many* transactions at once.

6 figures
end of lecture 1~69 min of figures
micro

The lock manager: the component that decides who waits and who runs

The lock manager: the component that decides who waits and who runs Three transactions each send a lock request to a central component, the lock manager. The lock manager holds a lock table showing who holds each row and who is waiting. It does three jobs: it grants or queues each request by checking shared/exclusive compatibility; the order it grants locks is the microschedule, the time-ordered record of who runs when; and it tracks the WaitsFor graph, aborting a victim if a cycle appears, which is deadlock. The lock manager: who waits, who runs Every transaction asks it for locks. It grants them, queues them, and watches for deadlock. T1 Req(A) T2 Req(A) T3 Req(B) LOCK MANAGER · THE LOCK TABLE Row A held by T1 (X) · waiting: T2, T3 Row B free → granted to T3 1 Grant or queue Checks shared/exclusive compatibility, then hands out the lock or makes you wait in line. 2 Builds the schedule The order it grants locks is the microschedule: the time-ordered record of who runs when. 3 Detects deadlock Tracks the WaitsFor graph. If a cycle appears, it aborts a victim to break it.
The lock manager again, all three jobs in one view. Job one, grant or queue, came on the Locks Stack page; this page is jobs two and three: the microschedule it builds as it grants locks, and the WaitsFor cycle it watches for. The rest of the page is those two jobs in motion.
Notes ↗
micro · Reading the Timeline: A Read or Write Takes Time

A read or write takes time, and interleaving fills the gap

A read or write takes time, and interleaving fills the gap Two transaction lanes over a time axis. T1 computes, then issues a read of B at Rs; the read travels to disk and back, an IO lag during which the CPU is idle, until it completes at Re; then T1 resumes. T2 runs in exactly that idle gap. The notation: Rs and Re are read start and read end, Ws and We are write start and write end, and the span between is the IO lag. A read or write takes time. Interleaving fills the gap. An operation has a start and an end because the data has to travel to disk and back. T1 computes Rs(B) read of B in flight, CPU idle Re(B) resumes T2 T2 runs here, in T1's idle gap Rs Re the IO lag time Rs / Re = read start / end  ·  Ws / We = write start / end  ·  the span between is the IO lag
Every read and write on the lock manager's timelines below is drawn as a start and an end (Rs to Re, or Ws to We) because the data has to travel to storage and back. While T1's read of B is in flight, the CPU is idle, so the lock manager grants T2 in that gap. The whole point of a microschedule is to make those overlaps visible.
Notes ↗
micro · Example 1: Serial Schedule (S1)

Serial lock schedule T2 → T1 → T3 (completion 49 units)

Serial lock schedule T2 → T1 → T3 (completion 49 units) A lock schedule timeline with each transaction on its own row and time on the X-axis (Req and Get as up-arrows, read/write start and end as boxes). It runs T2 first (times 1 to 9), then T1 (10 to 24), then T3 (25 to 48), with no overlap, finishing at 49 time units, the serial baseline the interleaved schedules must beat. Serial lock schedule T2 → T1 → T3 (completion 49 units) T1 T2 T3 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 Time Units Req(A) 1 Get(A) 2 Rs(A) 3 Re(A) 8 Unl(A) 9 Req(A) 10 Get(A) 11 Rs(A) 12 Re(A) 17 Ws(A) 18 We(A) 23 Unl(A) 24 Req(B) 25 Get(B) 26 Rs(B) 27 Re(B) 32 Ws(B) 33 We(B) 38 Req(A) 39 Get(A) 40 Ws(A) 41 We(A) 46 Unl(B) 47 Unl(A) 48 49 time units serial baseline
Serial schedule S1 runs T2, then T1, then T3 with no overlap. It is correct by construction but slow: 49 time units end to end, the baseline the interleaved schedules below have to beat. (The serial order is whichever the lock manager picks; any order is equally correct.)
Notes ↗
micro · Example 2: Interleaved Schedule (S2) - Fast Execution

Interleaved lock schedule (completion 31 units)

Interleaved lock schedule (completion 31 units) A lock schedule timeline with each transaction on its own row and time on the X-axis (Req and Get as up-arrows, read/write start and end as boxes), the per-transaction locks, and a WaitsFor graph. A red cycle in the WaitsFor graph is a deadlock. Interleaved lock schedule (completion 31 units) T1 T2 T3 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Time Units Req(A) 1 Get(A) 10 Rs(A) 11 Re(A) 16 Ws(A) 17 We(A) 22 Unl(A) 23 Req(A) 2 Get(A) 3 Rs(A) 4 Re(A) 9 Unl(A) 10 Req(B) 5 Get(B) 6 Rs(B) 7 Re(B) 12 Ws(B) 13 We(B) 18 Req(A) 19 Get(A) 23 Ws(A) 24 We(A) 29 Unl(A) 30 Unl(B) 31 Per-Transaction Locks T1 A: {"Req": 1, "Get": 10, "Unl": 23} T2 A: {"Req": 2, "Get": 3, "Unl": 10} T3 B: {"Req": 5, "Get": 6, "Unl": 31} A: {"Req": 19, "Get": 23, "Unl": 30} WaitsFor Graph T3 T1 T2 A: {"Req": 19, "Get": 23, "Unl": 30} A: {"Req": 1, "Get": 10, "Unl": 23} T1 holds A T3 waits A (19-23) A: {"Req": 19, "Get": 23, "Unl": 30} A: {"Req": 1, "Get": 10, "Unl": 23} A: {"Req": 1, "Get": 10, "Unl": 23} A: {"Req": 2, "Get": 3, "Unl": 10} T2 holds A T1 waits A (3-10)
When the lock manager interleaves the same transactions, S2 finishes in 31 units, well under the serial 49. Overlapping the lock-free stretches produces the speedup.
Notes ↗
micro · Example 3: Interleaved Schedule (S3) - Different Microschedule

Interleaved lock schedule (completion 45 units)

Interleaved lock schedule (completion 45 units) A lock schedule timeline with each transaction on its own row and time on the X-axis (Req and Get as up-arrows, read/write start and end as boxes), the per-transaction locks, and a WaitsFor graph. A red cycle in the WaitsFor graph is a deadlock. Interleaved lock schedule (completion 45 units) T1 T2 T3 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 Time Units Req(A) 5 Get(A) 24 Rs(A) 26 Re(A) 31 Ws(A) 32 We(A) 37 Unl(A) 38 Req(A) 1 Get(A) 38 Rs(A) 39 Re(A) 44 Unl(A) 45 Req(B) 2 Get(B) 3 Rs(B) 4 Re(B) 9 Ws(B) 10 We(B) 15 Req(A) 16 Get(A) 17 Ws(A) 18 We(A) 23 Unl(A) 24 Unl(B) 25 Per-Transaction Locks T1 A: {"Req": 5, "Get": 24, "Unl": 38} T2 A: {"Req": 1, "Get": 38, "Unl": 45} T3 B: {"Req": 2, "Get": 3, "Unl": 25} A: {"Req": 16, "Get": 17, "Unl": 24} WaitsFor Graph T2 T1 T3 A: {"Req": 1, "Get": 38, "Unl": 45} A: {"Req": 5, "Get": 24, "Unl": 38} T1 holds A T2 waits A (24-38) A: {"Req": 1, "Get": 38, "Unl": 45} A: {"Req": 5, "Get": 24, "Unl": 38} A: {"Req": 1, "Get": 38, "Unl": 45} A: {"Req": 16, "Get": 17, "Unl": 24} T3 holds A T2 waits A (17-24) A: {"Req": 1, "Get": 38, "Unl": 45} A: {"Req": 16, "Get": 17, "Unl": 24} A: {"Req": 5, "Get": 24, "Unl": 38} A: {"Req": 16, "Get": 17, "Unl": 24} T3 holds A T1 waits A (17-24)
S3 is a different valid interleaving of the same transactions, yet it finishes in 45 units, barely better than serial. Interleaving helps only when it overlaps the right operations; the order the lock manager grants in matters, not just the decision to interleave.
Notes ↗
micro · Example 4: Deadlock Detection

Deadlock: 2 transactions, circular wait

Deadlock: 2 transactions, circular wait A lock schedule timeline with each transaction on its own row and time on the X-axis (Req and Get as up-arrows, read/write start and end as boxes), the per-transaction locks, and a WaitsFor graph. A red cycle in the WaitsFor graph is a deadlock. Deadlock: 2 transactions, circular wait T1 T2 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Time Units Req(A) 1 Get(A) 2 Rs(A) 3 Re(A) 8 Ws(A) 9 We(A) 14 Req(B) 15 Req(B) 4 Get(B) 5 Rs(B) 6 Re(B) 11 Ws(B) 12 Req(A) 16 Per-Transaction Locks T1 A: {"Req": 1, "Get": 2} B: {"Req": 15} T2 A: {"Req": 16} B: {"Req": 4, "Get": 5} WaitsFor Graph T1 T2 B: {"Req": 15} B: {"Req": 4, "Get": 5} T2 holds B T1 waits B (15, _) B: {"Req": 15} B: {"Req": 4, "Get": 5} A: {"Req": 16} A: {"Req": 1, "Get": 2} T1 holds A T2 waits A (16, _) WaitsFor cycle T2 ↔ T1. DEADLOCK: neither transaction can proceed.
The same transaction pattern under different timing deadlocks: each transaction holds a ticket the other needs, so the lock manager's WaitsFor graph forms a cycle and neither can proceed. Interleaving buys performance only until a cycle stops everything.
Notes ↗
M4

Multi-Version Concurrency Control (MVCC)

Locking makes a reader wait for a writer to finish.

4 figures
mvcc · A familiar example: git

When you pulled decides what you see.

When you pulled decides what you see. A timeline reads left to right as one repository changes over time. Version v1 is the base. Engineer B pulls early and gets v1. Engineer C then commits a change, which adds version v2, equal to v1 plus the diff, drawn in violet. Engineer D pulls afterward and gets v2, while Engineer B, who pulled before the commit, still holds v1 and is unaffected. Color key: violet = C's change and the new v2; grey = the older base v1. When you pulled decides what you see One repository changes over time. Your checkout is frozen at the moment you pulled. Color key: violet = C's change and the new v2 · grey = the older base v1. Repo time → v1 · base pull Engineer B · has v1 pulled before the commit, unaffected Engineer C commits a change commit diff v2 = v1 + diff pull Engineer D · has v2 pulled after, sees C's change
One repository changes over time, and your checkout is frozen at the moment you pulled. Engineer C commits a change, which adds v2 (v1 plus the diff, violet). Engineer D pulls afterward and gets v2; Engineer B pulled earlier, so B still has v1 and C's change does not touch B. Same repository, but when you pulled decides what you see.
Notes ↗
mvcc · The snapshot

An uncommitted write is invisible until it commits.

An uncommitted write is invisible until it commits. A timeline of one value V. V holds AB1, committed at 2:00. Minnie starts writing XY34 at 2:45 but does not commit until 3:03, drawn as a dashed violet in-flight span above the line. Inside that window the committed value is still AB1, so Pluto with snapshot 2:50 and Mickey with snapshot 3:00 both read AB1, never the half-written XY34. A query with snapshot 3:05 reads XY34 after the commit. Color key: violet = Minnie's write and XY34 once it commits; grey = the old value AB1, what every read before the commit returns. An uncommitted write is invisible until it commits Minnie updates V at 2:45 and commits XY34 at 3:03. Until the commit, every read still returns AB1. Color key: violet = Minnie's write and XY34 once it commits · grey = AB1, what every read before the commit returns. Value V time → AB1 · 2:00 starts 2:45 Minnie writing XY34 (uncommitted) commits 3:03 reads Pluto · snapshot 2:50 reads AB1, write in flight reads Mickey · snapshot 3:00 reads AB1, still uncommitted XY34 · 3:03 reads Query · snapshot 3:05 reads XY34, now committed
Minnie updates V at 2:45 but does not commit XY34 until 3:03. In that in-flight window the committed value is still AB1, so Pluto (snapshot 2:50) and Mickey (snapshot 3:00) both read AB1, never the half-written XY34. Only a query whose snapshot falls after the commit, like 3:05, reads XY34. The write becomes visible at the commit, not when it starts.
Notes ↗
mvcc · Vacuum: reclaiming dead versions

Vacuum deletes the row versions that no running query still reads

Vacuum deletes the row versions that no running query still reads One row keeps four versions, oldest to newest (v1 to v4), each a value plus a commit timestamp. Two queries are still running: one opened at 2:50 and still reads v3, and a newer one reads the current v4. No running query reads v1 or v2, so they are dead. Vacuum deletes the dead versions and frees the space, leaving v3 and v4. Color key: violet is a version a running query still reads; grey is a dead version vacuum deletes. Vacuum: delete versions no query still reads Each update leaves the old version behind. A version stays while a running query reads it; vacuum deletes the rest. Row: song 42 likes · one version per update → v1 @ 2:00 likes = 100 v2 @ 2:20 likes = 101 v3 @ 2:45 likes = 102 v4 @ 3:10 · now likes = 103 query @ 2:50 newer query DEAD no running query reads these LIVE v3 still read, v4 is current v1 @ 2:00 likes = 100 v2 @ 2:20 likes = 101 VACUUM discards v1 and v2 → space freed violet = a version a running query still reads  ·  grey = dead version, deleted by vacuum
A version is one value plus a timestamp, not a copy of the row. MVCC must keep an old version while any running query still reads it: here the query that opened at 2:50 still reads v3, and newer queries read v4, but nothing reads v1 or v2. Vacuum deletes the versions no running query needs, so the chain does not grow forever.
Notes ↗
mvcc · The payoff: why OLTP and OLAP get separated at scale

Why OLTP and OLAP get physically separated at scale

Why OLTP and OLAP get physically separated at scale MVCC removes lock contention between readers and writers, but a long analytical scan still strains the production database's shared resources: it saturates the CPU and IO that user-facing queries need, pollutes the buffer cache, and pins VACUUM so tables bloat. None of these is a lock problem. The fix is physical separation: change-data-capture replicates the data to a separate columnar warehouse where analytics runs, and the OLTP database stays user-facing only. Color key: violet is the user-facing OLTP database, red is the strain a scan would put on it, grey is the separate analytics warehouse. Keep analytics off the production database MVCC ends the lock fight, but a big scan still strains shared resources. OLTP · Postgres user-facing, 100 ms queries A 10-minute scan here would: saturate CPU & IO pollute the buffer cache pin VACUUM, bloat tables replicate (CDC) OLAP warehouse columnar · BigQuery, Snowflake analytics runs here the OLTP database stays user-facing only violet = production OLTP  ·  red = the strain a scan adds  ·  grey = the separate warehouse
MVCC removes the reader-writer lock contention, but a long analytical scan still strains shared resources on the production database: it saturates CPU and IO, pollutes the buffer cache, and pins VACUUM so tables bloat. None is a lock problem, so the fix is physical: replicate the data to a separate columnar warehouse for analytics and keep the OLTP database user-facing only.
Notes ↗
M4

Optimistic Concurrency Control

Optimistic concurrency control (OCC) runs transactions with no locks.

1 figure
occ

Optimistic concurrency: run free, validate at commit, redo on a clash

Optimistic concurrency: run free, validate at commit, redo on a clash Two transactions run with no locks. Each has three phases: a read phase that computes on a private copy, a validate phase at commit, and a write phase. T1 validates clean (nothing it read changed) and commits, in green. T2 ran over the same window, but at validate a row it read had been written by T1, so it fails validation (red), aborts, and restarts its read phase. Color key: violet is a running phase, green is a clean validation and commit, red is a conflict and abort. Optimistic concurrency: run free, check at commit No locks while running. Validate at the end; a clash means redo the work, not wait. THREE PHASES: READ (private copy) → VALIDATE (at commit) → WRITE T1 READ · compute on a private copy VALIDATE ✓ WRITE → committed T2 READ · compute on a private copy VALIDATE ✗ a row T2 read was written by T1 abort READ again (retry), then validate and commit The validate check, run once at commit Did anything I read get written by a transaction that committed since I started? No → commit. Yes → abort & retry. Under two-phase locking, T2 would have waited for T1. Optimistic lets it run, and pays only on a real clash, so it wins when conflicts are rare. Color key violet = a running phase · green = clean validation, commit · red = a conflict, abort.
Both transactions run lock-free and check only at commit. T1 validates clean and commits (green); T2 finds a row it read was written by T1, so it fails validation (red), aborts, and reruns. Under two-phase locking T2 would have waited; here the cost of never waiting is redoing the work on a clash.
Notes ↗
M4

Connecting It All Together

A complete transaction system needs a correctness standard, *conflict-serializability*, and engines that target it.

1 figure
connecting-together · The Power of Transaction Managers: Beyond Simple Locks

The same two-phase locking, at every scale

The same two-phase locking, at every scale Once the transaction-manager pieces exist, the same protocol stretches three ways. It distributes: one transaction can lock rows on machines in different cities and the manager holds two-phase locking across all of them. It recovers: the recovery manager replays the write-ahead log after a crash so committed work survives. And it scales: stock exchanges and bank ledgers push millions of transactions a second through this exact idea. Color key: violet is the shared two-phase-locking core under all three. The same lock, at every scale One protocol, two-phase locking, at three different operating points. Distributes across machines one transaction locks rows in New York, San Francisco, Tokyo, 2PL held across all three Recovers from a crash the recovery manager replays the write-ahead log, so committed work survives a crash Scales to millions per second stock exchanges and bank ledgers run the same idea, millions of transactions a second violet = the same two-phase-locking core under all three
The same protocol stretches three ways. It distributes, holding two-phase locking across machines in different cities; it recovers, replaying the write-ahead log after a crash so committed work survives; and it scales, running millions of transactions a second on stock exchanges and bank ledgers. Same core, different operating point.
Notes ↗
M4

Change Tracking: Log the Diff, Don't Copy

A database keeps a log of what changed, never a copy of the whole database.

3 figures
recovery-intuition · The Version Problem

The version problem: everyone needs a past version, but full copies explode under concurrency, so a database logs the change instead

The version problem: everyone needs a past version, but full copies explode under concurrency, so a database logs the change instead Two panels. On the left, the problem in red: to keep every past version you would copy the whole database on every change, and with millions of accounts changing thousands of times a second that is billions of rows per second, impossible. On the right, the answer in violet: log only the change, one small entry each, and replay the entries to reconstruct what was true at any past moment. Color key: red = copying every version, which cannot scale; violet = the change log and the version it rebuilds; grey = the full-database copies you avoid. The version problem: keep the past without copying it Millions of accounts, thousands of changes a second. You need any past version, but full copies explode. Snapshot every version copy all accounts on every single change whole database a fresh copy per change billions of rows per second, impossible Log the change one small entry per change, not a copy 10:31:04 acct 12 cash 5000 → 4820 10:31:04 acct 88 cash 210 → 640 10:31:05 acct 12 shares 3 → 4 Replay to any moment What was acct 12 at 10:31:04? Sum the entries up to then. Any past version, rebuilt, with no full copy stored. red = copying every version, cannot scale  ·  violet = the change log and the version it rebuilds  ·  grey = copies avoided
Keeping every version by copying the whole database (red) is billions of rows a second under real load, impossible. Logging each change instead (violet) is a handful of small entries, and any past version replays from them, 10:31:04 included. You reach any moment without ever storing a full copy.
Notes ↗
recovery-intuition · You Already Use It

Google Docs version history is a log of edits, not fifty stored copies.

Google Docs version history is a log of edits, not fifty stored copies. A Google Doc evolves left to right. The history panel does not hold fifty full copies of the document. It appends one entry per edit: typed a sentence, deleted a paragraph, fixed a typo. Docs keeps the current document and snapshots itself now and then, so scrolling the history replays edits from a recent snapshot, not from the first keystroke, to reconstruct any past version. This is exactly the model a database uses for crash recovery. Color key: violet = the appended edit entries, the diffs; grey = the full-copy approach it avoids. Google Docs logs edits, not a copy of every version Each version-history line is one change. Scroll the history and Docs replays the edits. Version history (edit log) 10:02 typed a sentence + "Recovery logs the diff." 10:05 deleted a paragraph - intro draft 10:09 fixed a typo "teh" to "the" replay the edits Any past version Docs keeps the current doc and logs each edit, three small entries, not three full document copies. Same idea as a database log. Color key: violet = the appended edit entries, the diffs · grey = the deleted text and the full-copy approach Docs avoids.
Google Docs keeps periodic snapshots and logs every edit between them, violet, not a full copy of every version. Any past version replays from a nearby snapshot forward. The same move a database log makes: save the change, reach any version.
Notes ↗
recovery-intuition · You Already Use It

A git commit is a snapshot that shares unchanged files, not a full copy. Move forward to redo, backward to undo.

A git commit is a snapshot that shares unchanged files, not a full copy. Move forward to redo, backward to undo. Three commits sit on a branch left to right. Each commit is a snapshot, but files that did not change are shared, not recopied, so a commit is never a full copy of the repository. You move to any commit to go forward, or revert one to go backward. Moving forward redoes committed work; moving backward undoes it. A database recovery log gives the same two moves. Color key: violet = the commits, each recording what changed with author and time; green = moving forward to redo; red = moving backward to undo. A commit is a snapshot, not a full copy Git stores which lines changed, when, and by whom. Replay the diffs to reach any state. time commit a1b2 + schema.sql by Mia, Mon shares unchanged files commit c3d4 ~ 4 lines by Theo, Tue shares unchanged files commit e5f6 - dead code by Mia, Wed shares unchanged files replay forward to REDO committed work replay backward to UNDO it Color key: violet = the commits, each a diff with author and time · green = replay forward to redo · red = replay backward to undo.
A Git commit is a snapshot, not a full copy: files that did not change are shared, so history stays small. Moving forward to a later commit redoes work, green; reverting one undoes it, red. A database recovery log gives the same two moves.
Notes ↗
M4

The WAL Log

Every update is three writes, in order.

5 figures
recovery-concepts · The Lifecycle of an Update

The lifecycle of one update: append to the log, update the RAM page, flush the page to disk later

The lifecycle of one update: append to the log, update the RAM page, flush the page to disk later One update, value A going from 19 to 22.8, is three writes in time order. First it appends a tiny old-to-new diff to the write-ahead log, violet, durable and sequential. Second it updates A's page in the RAM buffer, red, in place, drawn as a grid of 64 MB page slots; RAM is volatile. Both happen now. Third, later, the whole 64 MB page is flushed to disk, green, a grid of durable page slots, as one write from the start of the page to its end. The log record is durable before the page ever reaches disk, which is the write-ahead rule. Color key: violet is the durable sequential log, red is the volatile RAM buffer, green is the durable disk, grey is page structure. The lifecycle of an update Three writes, in order: append to the log, update the RAM page, flush the page to disk later. A page is 64 MB (big-data systems); a log record is a few dozen bytes. the update value A 19 → 22.8 WAL log tracks old and new · a few tens of bytes (~40 B) RAM buffer 64 MB pages · volatile, lost on a crash Disk (data file) 64 MB pages · durable, slow 19 of 2048 pages ① append A: 19 → 22.8 ② update A 22.8 dirty page ③ flush later A time ① append to log durable now ② update RAM page now ③ flush page to disk later · one whole-page write (Ws → We) The log record is durable before the page ever reaches disk. That is write-ahead. Color key violet = durable, sequential log · red = volatile RAM buffer · green = durable disk · grey = page structure
One update, three writes in time order. ① Append the diff to the log (violet), durable at once; ② update A's page in the RAM buffer (red) in place, fast but volatile; ③ later, flush the whole 64 MB page to disk (green) as one write, start to end. Steps ① and ② happen now and the flush is deferred, so the log is always durable before the page reaches disk.
Notes ↗
recovery-concepts · Inside the WAL Row

The WAL log as a table of diffs for T1

The WAL log as a table of diffs for T1 The log after T1 raises A and B by twenty percent. It is a growing append-only table: each write is one row holding the old value, orange, that undo needs and the new value, green, that redo needs. T1 writes A from 19 to 22.8 and B from 23 to 27.6, then a COMMIT record marks the transaction's end. Ellipsis rows show this is one stretch of a much longer log. Color key: orange is the old value, green is the new value, violet is the log itself. The log: a table of diffs Each write is one row of old and new. A commit record ends the transaction. Txn Var old value new value T1 A 19 22.8 T1 B 23 27.6 T1 COMMIT record — T1 ends here Color key orange = old value (undo) · green = new value (redo) · violet = the log The log only grows: every change appends one row, and the commit record closes the transaction. Each row ≈ 40 B: header 12 · txn 4 · location 8 · old 8 · new 8.
The log after two updates: A from 19 to 22.8 and B from 23 to 27.6. Each is one row with the old value (orange) and the new value (green), so the same log carries what it takes to put a change back or take it away. A commit record closes the transaction. These are the rows undo runs against when a transaction aborts, and redo when a crash hits.
Notes ↗
recovery-concepts · Any Version, on Demand

The WAL log is the version history: replay it to any past state

The WAL log is the version history: replay it to any past state The log drawn top to bottom in time, each row an old-to-new change: T1 sets A to 22.8 and B to 27.6, then later T2 and T3 change them again. To see the state at any past moment, stop replaying the rows there. Stopping just after T1 gives the version A=22.8, B=27.6. Replaying forward uses the new values, stepping back uses the old, so the log reaches any version either way. Color key: orange is the old value, green is the new value, violet is the log. The log is the version history Every row holds old and new, so you can replay the log to any past state. Txn Var old value new value T1 A 19 22.8 T1 B 23 27.6 T2 A 22.8 30 T3 B 27.6 33 time ↓ replay up to here The version at that moment A = 22.8 B = 27.6 forward: redo the new values back: undo the old values Color key orange = old value · green = new value · violet = the log
The log read top to bottom is a full change history. To see the state at any past moment, stop replaying the rows there: just after T1, the version is A=22.8, B=27.6. Because each row carries both values, you can move either way, redoing new values forward or undoing old values back.
Notes ↗
recovery-concepts · Cheap for Two Reasons: Small and Sequential

WAL turns scattered random writes into one sequential append

WAL turns scattered random writes into one sequential append A comparison of two ways to write the same changes to disk. On the left, without the log, each change is a random I/O to its own scattered 64 MB data page, about 13 ms, shown in grey. On the right, the WAL log appends every change as a tiny row, about 40 bytes, to one growing file shown in violet: a 64 KB WAL page costs about 13 microseconds, roughly a thousand times less, and a row in battery-backed NVRAM is durable in about 100 nanoseconds, a hundred thousand times less than the page flush. Small and sequential beats big and random. Color key: grey is the big scattered data pages, violet is the tiny sequential log. Small and sequential, not big and random A WAL row is about 40 bytes and only appends; a data page is 64 MB and needs a random I/O. Without the log: big and scattered page 17 page 492 page 1038 Each is a 64 MB page, a random I/O. ~13 ms. With the log: tiny and appended T1 A 19 22.8 T1 B 23 27.6 … each row appends at the tail ↓ Each row is about 40 B, right after the last. 64 KB WAL page: ~13 µs. Color key grey = the big scattered data pages · violet = the tiny sequential log 64 MB page flush ~13 ms · 64 KB WAL page ~13 µs (~1,000× less) · NVRAM row ~100 ns (~100,000× less).
Without the log (grey), each change is a random I/O to its own scattered 64 MB data page, about 13 ms. With the log (violet), every change is a ~40 byte row appended to one growing file, one fast sequential stream: a 64 KB WAL page costs about 13 µs (~1,000× less), and a row in battery-backed NVRAM is durable in about 100 ns (~100,000× less). Small and sequential beats big and random.
Notes ↗
recovery-concepts · Why the Log Goes First

Why log-before-data makes a crash recoverable

Why log-before-data makes a crash recoverable Two orderings of the same two disk writes: the data page, drawn as a page block, and the log record, drawn as a WAL table row with columns Txn, Var, old value in orange and new value in green. On the left, data first: the crash hits before the log row lands, so the data page changed with no record of why, an unrecoverable state shown in red. On the right, log first as WAL demands: the crash hits before the data page moves, so the intent is in the durable log and recovery re-applies it, shown in green. Color key: red is the unrecoverable order, green is the recoverable WAL order, violet is the durable log. Why log-before-data The same two writes, two orders. Only one survives a crash. Data first A 22.8 data page, on disk CRASH Txn Var old new T1 A 19 22.8 log row, never lands Data changed, nothing remembers why. Unrecoverable. Log first (WAL) Txn Var old new T1 A 19 22.8 log row, on disk CRASH A 22.8 data page, not yet moved Intent is in the durable log, replay it. Recoverable. Color key red = the unrecoverable order · green = the recoverable WAL order · violet = the durable log
Data first (red): if the crash hits before the log row lands, the data changed with no row to replay, an inconsistency recovery cannot fix. Log first (green), the WAL order: a crash before the data page moves leaves the change sitting in the durable log, ready to re-apply. Log-before-data is the only order that keeps the data file recoverable.
Notes ↗
M4

Undo for Aborts

A running transaction can abort, by choice or by error, and the database has to take back every change it made so it is as if the transaction never ran.

2 figures
recovery-wal · How Undo Works

Undo is logged: it appends W-abort rows to the log

Undo is logged: it appends W-abort rows to the log The same WAL rows as the WAL Log page: T1 wrote A from 19 to 22.8 and B from 23 to 27.6. When T1 aborts, undo does not just fix the data page, it appends its reversal to the log, newest write first: a W-abort row for B from 27.6 back to 23, then a W-abort row for A from 22.8 back to 19, then an abort record. The data page ends at A=19, B=23. Because each W-abort row is itself a logged write, the log records the whole rollback. Color key: orange is the old value, green is the new value, red is undo's own W-abort rows. Undo is a write too: it appends W-abort rows On abort, write each old value back and record the reversal in the log as W-abort rows. Txn Action Var old value new value T1 W A 19 22.8 T1 W B 23 27.6 abort: no commit, so undo walks back and logs each reversal T1 W-abort B 27.6 23 T1 W-abort A 22.8 19 T1 abort Undo is logged, not just applied The data page ends at A=19, B=23. Each reversal is itself a logged write, so the log records the whole rollback. Color key orange = old value · green = new value · red = undo's own W-abort rows
T1's rows from the WAL Log page. When T1 aborts, undo does not just fix the data page, it appends its reversal to the log: a W-abort row for B (27.6 back to 23), then A (22.8 back to 19), then an abort record. The page ends at A=19, B=23. Because each reversal is itself a logged write, the log records the whole rollback, W-abort rows and all.
Notes ↗
recovery-wal · The Hard Case: Uncommitted Data on Disk

The log for a long transaction TLong that aborts

The log for a long transaction TLong that aborts A log table for TLong, which writes A0 and A1000 then aborts. The two forward writes raise A0 from 19 to 22.8 and A1000 from 27 to 32.4. Undo then walks the log backward, newest change first: it logs a W-abort that restores A1000 from 32.4 to 27, then a W-abort that restores A0 from 22.8 to 19, then a final abort record. The old value column drives each undo. Color key: violet is the log, red marks the undo writes that reverse the transaction, green is the restored data on disk. The log: TLong aborts and unwinds Undo does not erase log rows. It appends W-abort rows, newest write undone first. Time Txn Action Var old value new value disk state after 13 TLong W A0 19 22.8 A0 = 22.8 92 TLong W A1000 27 32.4 A1000 = 32.4 1000 TLong W-abort A1000 32.4 27 A1000 = 27 1000 TLong W-abort A0 22.8 19 A0 = 19 1000 TLong abort back to start state Undo is just a write Each W-abort reuses the old value from the forward row: 32.4 back to 27, then 22.8 back to 19. Each W-abort is itself logged, so the log records TLong's full rollback. Color key violet = the log · red = the undo writes that reverse TLong · green = the restored data on disk Recovery walks backward: A1000 is undone before A0, newest write first.
TLong aborts. Undo walks the log backward, newest write first, restoring A1000 from 32.4 to 27, then A0 from 22.8 to 19, reusing the old value each forward row recorded. Each undo becomes its own W-abort row (red), so the log records the rollback itself. A final abort record closes the transaction.
Notes ↗
M4

Recovery Post-Crash

A crash leaves two messes on disk at once.

4 figures
recovery-postcrash

After a crash, recovery answers one question from the WAL: which work survives and which is thrown away

After a crash, recovery answers one question from the WAL: which work survives and which is thrown away A crash interrupts running transactions. When the server reboots it must answer one question, which work survives and which gets thrown away. The whole answer lives in the WAL, the durable record of every change. Committed transactions are kept, green; transactions that never committed are thrown away, orange; and the log, the source of truth, decides which is which. Color key: green = committed work that survives, orange = uncommitted work that is thrown away, grey = the durable log that decides. A crash leaves one question: which work survives? Power dies mid-transaction. On reboot, the WAL decides what is kept and what is discarded. Crash power dies, disk faults, network drops Several transactions were running. Some had committed. Some had not. The answer is in one place: the WAL. ✓ Survives Committed transactions. Once you saw COMMIT, that work is kept. recovery replays them so they reach disk for good ✗ Thrown away Transactions that never committed. Their writes are reversed. recovery undoes them so no half-done work remains Color key green = committed work that survives · orange = uncommitted work thrown away · red = the crash
A crash leaves recovery one question: which work survives, which gets thrown away. The whole answer is in the **WAL**. Committed transactions are kept, green; transactions that never committed are reversed, orange. Recovery is the procedure that reads the log and enforces both.
Notes ↗
recovery-postcrash · Recovery on One Log: Down, Then Up

Crash recovery on one WAL: a forward replay rebuilds the crash state, then a backward undo reverses the uncommitted transactions

Crash recovery on one WAL: a forward replay rebuilds the crash state, then a backward undo reverses the uncommitted transactions A WAL drawn top to bottom in time order, with the page log columns: Time, Txn, Action, Var, old value, new value. Start state A=10, B=20, C=30, D=40, E=50, update f(x)=x times 1.2. The figure runs in five stages. First the log and the start state. Then a red CRASH line marks where the server stopped. Then the REPLAY pass runs down the left re-applying every logged row, original writes and the in-log abort rows alike, rebuilding the exact crash state A=12, B=20, C=43.2, D=48, E=60. Then the UNDO pass runs up the right and reverses only the uncommitted transactions newest-first: E 60 to 50, C 43.2 to 36, C 36 to 30. Finally the consistent state A=12, B=20, C=30, D=48, E=50. T1 and T4 committed and are kept; T2 aborted explicitly and was already rolled back in the log; T3 and T5 never committed and are undone. Color key: violet = the replay and undo passes and the rows undo removes, blue = durability, the committed work the replay keeps, orange = atomicity, the uncommitted work the undo removes, grey = rows left untouched, red = the crash. Two passes over one log: replay down, then undo up Start A=10, B=20, C=30, D=40, E=50, update f(x) = x × 1.2. Five transactions run, then the server crashes. Time Txn Action Var old value new value 1 T1 W A 10 12 2 T3 W C 30 36 3 T1 COMMIT 4 T2 W B 20 24 5 T4 W D 40 48 6 T2 W-abort B 24 20 7 T2 ABORT 8 T3 W C 36 43.2 9 T4 COMMIT 10 T5 W E 50 60 Database state start A=10 B=20 C=30 D=40 E=50 CRASH — server stops here, recovery begins Disk holds whatever was written before the crash, not a consistent state. Replay rebuilds the crash state, then undo cleans it. REPLAY · every row, top to bottom crash A=12 B=20 C=43.2 D=48 E=60 every logged row re-applied 1 undo E: 60→50 2 undo C: 43.2→36 3 undo C: 36→30 UNDO · the uncommitted (newest-first) undone A=12 B=20 C=30 D=48 E=50 T3, T5 reversed; T2 already rolled back ⊙ Final, consistent: A=12 B=20 C=30 D=48 E=50 T1, T4 kept · T2, T3, T5 gone Committed (T1, T4): replayed and kept (durability).Never committed (T3, T5): undone now (atomicity).Explicit abort (T2): already rolled back in-log. Color key violet = the replay and undo passes · blue = committed work kept · orange = uncommitted work undone · red = the crash
Recovery as two passes over one log:
Notes ↗
recovery-postcrash · After the Crash: Fix the Disk from the WAL

Just after the crash: RAM is gone and the disk is an inconsistent mix of flushed and unflushed work

Just after the crash: RAM is gone and the disk is an inconsistent mix of flushed and unflushed work The instant after the crash. RAM is gone, every in-flight change with it. The disk survived but it is not consistent: it holds whatever was written before the power died. Some committed writes never flushed, so a committed value is missing. Some uncommitted writes did flush, so an uncommitted value is sitting on disk. The durable WAL is untouched and still holds the full history, which is how recovery sorts the mess out. Color key: red = RAM, now empty, green = the durable disk that is not yet consistent, violet = the durable, sequential WAL that survived and drives recovery. Just after the crash: the disk holds a half-written mix RAM is gone. The disk is not consistent: some committed work missing, some uncommitted work present. RAM · gone (empty) every in-flight change lost Disk · not consistent a half-written mix A=12 flushed committed (T1), did reach disk D=40 stale committed (T4) write never flushed E=60 wrong uncommitted (T5), but it flushed Committed work missing, uncommitted work present. WAL · durable, sequential full history, untouched A: 10→12 C: 30→36 D: 40→48 C: 36→43.2 E: 50→60 old and new values, enough to rebuild and to undo. Color key red = RAM, now empty · green = the durable disk, some values wrong · violet = the durable, sequential log
Just after the crash. RAM, red, is gone with every in-flight change. The disk, green, survived but is not consistent: a committed write is missing, and an uncommitted write is wrongly present. The WAL, violet, is untouched, and it is the only thing recovery can use, because RAM holds nothing.
Notes ↗
recovery-postcrash · Recovery Guarantees

Recovery restores two ACID guarantees across a crash: durability from the replay, atomicity from the undo pass, both resting on the write-ahead rule

Recovery restores two ACID guarantees across a crash: durability from the replay, atomicity from the undo pass, both resting on the write-ahead rule Two of the ACID guarantees fall out of the two passes. Durability, the D icon in violet, comes from the replay: committed work is replayed and never undone, so it survives any failure. Atomicity, the A icon in blue, comes from the undo pass: a transaction is all or nothing, so unfinished work is reversed across a crash. Both rest on the write-ahead rule at the base: the log reaches the disk before the data page, so the log is always enough to finish the story. Color key: violet = durability from replay, blue = atomicity from undo, grey = the write-ahead rule both rest on. Recovery restores durability and atomicity across a crash The replay gives durability, the undo pass gives atomicity. Both rest on the write-ahead rule. D Durability · from replay once you see COMMIT, the work lasts The replay redoes every row, and committed work is never undone, so it survives any failure. A Atomicity · from undo a transaction is all or nothing The backward pass reverses every transaction that never committed, so no half-done work survives the crash. ⊙ The write-ahead rule makes both hold The log reaches disk before the data, so the log is always enough to finish the story. Color key violet = durability from replay · blue = atomicity from undo · grey = the write-ahead rule both rest on
Two ACID guarantees fall out of the two passes, in the order the passes run. Durability, the violet D, comes from the replay: once you see COMMIT, that work is replayed and never undone, so it survives any failure. Atomicity, the blue A, comes from the undo pass: a transaction is all or nothing, so unfinished work is reversed. Both rest on the write-ahead rule at the base, the log reaching disk before the data.
Notes ↗
M4

Transaction Manager: The Architecture

A Transaction Manager is the orchestrator that runs scheduling, locking, write-ahead logging, deadlock detection, and recovery as one cooperating system, making thousands of concurrent transactions look like each ran alone, with t

3 figures
tm-summary · The Architecture

Transaction Manager architecture: incoming transactions flow through five cooperating components into one consistent ACID database state

Transaction Manager architecture: incoming transactions flow through five cooperating components into one consistent ACID database state Incoming user transactions enter the Transaction Manager, a violet container that holds five components, each owning one ACID guarantee. The Scheduler interleaves operations for a conflict-serializable order (Consistency). The Lock Manager enforces 2PL with O(1) hash lookups (Isolation). The Write-Ahead Log records every change before the data page moves (Durability). The Deadlock Detector watches the waits-for graph and aborts a victim. The Recovery Manager runs Analysis, REDO, and UNDO after a crash (Atomicity). The output is a consistent database state. Color key: violet is the Transaction Manager and the scale axis; green is a durability or correctness guarantee; blue is the isolation boundary; amber is the deadlock caution; red is the atomicity rollback path; grey is out of focus. The Transaction Manager: five components, one guarantee Each component owns one ACID property. Together they make concurrent transactions look serial and crash-proof. Transactions T1, T2, T3 … BEGIN COMMIT / ABORT TRANSACTION MANAGER 1 · Scheduler Interleaves operations into a conflict-serializable order: the result equals some serial run. C Consistency 2 · Lock Manager Enforces 2PL on shared tuples, hash table for O(1) lock lookups. Blocks only on overlap. I Isolation 3 · Write-Ahead Log Logs every change before the data page moves. Sequential writes, old and new values for undo and redo. D Durability 4 · Deadlock Detector Watches the waits-for graph for a cycle, then aborts one victim to break the deadlock. guard 5 · Recovery Manager After a crash: Analysis finds what was running, REDO replays committed work, UNDO reverses the incomplete transactions. All of a transaction, or none. A Atomicity DB State ACID consistent at every moment Color key  green = a correctness or durability guarantee  ·  blue = the isolation boundary  ·  amber = the deadlock guard red = the atomicity rollback path  ·  violet = the Transaction Manager that holds them all
Five components, one guarantee. The scheduler keeps the interleaving conflict-serializable (Consistency); the lock manager enforces 2PL with O(1) hash lookups (Isolation); the WAL logs every change before the data page moves (Durability); the deadlock detector watches the waits-for graph and aborts a victim on a cycle; the recovery manager runs Analysis, REDO, and UNDO after a crash (Atomicity). Together they turn concurrent transactions into one consistent state.
Notes ↗
tm-summary · Five Components, Four Firing

A no-conflict trace: three transactions on different tuples flow through the Transaction Manager, four of the five components fire

A no-conflict trace: three transactions on different tuples flow through the Transaction Manager, four of the five components fire Three transactions arrive at the same instant: Mickey buys Premium (blue), Minnie updates her profile (orange), Daffy adds a song (violet). They touch different tuples. The Scheduler routes each to a thread. The Lock Manager grants all three locks with no waiting because the tuples do not overlap. The Write-Ahead Log records each change before commit. The Deadlock Detector stays quiet, the only one of the five that does not fire, because there is no waits-for cycle. The Recovery Manager stands by. The result is one consistent committed state. Color key: blue is Mickey's transaction, orange is Minnie's, violet is Daffy's; green is a component that fired; grey is the Deadlock Detector, idle on this trace. One no-conflict trace: five components, four firing Three transactions on different tuples. The deadlock detector has nothing to do. Mickey · buys Premium UPDATE Users SET is_premium Minnie · updates profile UPDATE Users SET name Daffy · adds a song INSERT INTO Songs Scheduler Routes each transaction to its own thread, interleaves for throughput. Lock Manager Mickey row · granted Minnie row · granted Daffy row · granted No overlap, so no waiting. Write-Ahead Log LOG Mickey, Minnie, Daffy Each change logged before its commit becomes durable. Deadlock Detector Waits-for graph is empty. No cycle, so no victim. Stays quiet on this trace. Recovery Manager Stands by. If the server crashed here, it would replay the log to a consistent state. Committed state Mickey premium · Minnie renamed · Daffy's song added ACID preserved Color key  blue = Mickey  ·  orange = Minnie  ·  violet = Daffy green = a component that fired  ·  grey = the deadlock detector, idle on this no-conflict trace
One coherent execution. The scheduler routes each transaction to a thread, the lock manager grants all three locks with no waiting (the rows do not overlap), and the WAL logs every change before its commit becomes durable. The deadlock detector stays quiet because there is no waits-for cycle, and the recovery manager only acts if the server crashes. ACID preserved.
Notes ↗
tm-summary · Why It Scales

Why the Transaction Manager scales: most transactions touch different data, so few of them ever conflict

Why the Transaction Manager scales: most transactions touch different data, so few of them ever conflict Out of one thousand concurrent transactions, only ten to twenty actually conflict on shared rows. The rest touch independent data and never wait. Three design choices make that cheap: the lock manager uses a hash table for O(1) lock checks, the write-ahead log turns random writes into one sequential stream, and each component does one job and talks to the others through a tiny interface. The takeaway is that separation of concerns lets the manager run a million transactions per second on commodity hardware. Color key: green is the independent transactions that never wait; amber is the small set that actually conflicts; violet is the three design choices; ink is the takeaway. Why it scales: most transactions never touch the same row In a database with a million accounts, account #47893 has nothing to do with account #82456. ~980 transactions · independent Touch different rows. Locks granted at once, no waiting. ~10–20 transactions · actually conflict Share a row. Only these queue and need the lock manager to serialize. two percent of the load does all the waiting O(1) lock checks The lock manager keeps a hash table, so checking a lock is constant time at any scale. Sequential WAL Random updates everywhere become one append-only log stream, the fastest pattern a disk has. Separation of concerns Each component does one job and talks through a tiny interface. The scheduler never touches the disk. ⊙ Takeaway Because conflicts are rare and each component is cheap and independent, the manager runs a million transactions per second on commodity hardware. Color key  green = the independent transactions that never wait  ·  amber = the few that conflict  ·  violet = the design choices  ·  ink = the takeaway
Most transactions never touch the same row, so most never wait. Out of roughly a thousand concurrent transactions, only ten to twenty actually conflict. A hash table makes each lock check O(1), the WAL turns scattered updates into one sequential stream, and each component does one job behind a tiny interface. That separation of concerns lets a database run a million transactions per second on commodity hardware.
Notes ↗
M4

Three Transactions, Three Variables

Run three concurrent transactions through the whole Transaction Manager at once and watch the components cooperate: the scheduler interleaves them for speed, the lock manager keeps them off each other's rows, and the WAL makes eve

3 figures
tm-three-transactions · The Setup: T1, T2, T3 Operating on A, B, C

The interleaved schedule of T1, T2, T3 on A, B, C

The interleaved schedule of T1, T2, T3 on A, B, C A schedule shown as a table of Transaction, Action, Variable, Step, and Value, one row per read or write in step order. T1 touches A, T2 touches A, T3 touches B then C; the three interleave under Strict 2PL. Reads are blue, writes are orange. The database starts at A=19, B=32, C=27 and, if all commit, ends at A=27.36, B=38.4, C=32.4. One interleaved schedule, three transactions Start values: { A = 19, B = 32, C = 27 } · Strict 2PL: each lock is held until COMMIT or ABORT Transaction Action Variable Step Value T2 R A 1 19 T2 W A 2 22.8 T3 R B 3 32 T1 R A 4 22.8 T3 W B 5 38.4 T1 W A 6 27.36 T3 R C 7 27 T3 W C 8 32.4 If all three commit: { A = 27.36, B = 38.4, C = 32.4 } Disjoint rows: T1 and T2 share A, T3 owns B and C. They interleave for speed. Color key: R = a read · W = a write · Value = the variable after that step
One interleaved schedule, eight reads and writes in step order. T1 and T2 share A, T3 owns B and C, so the scheduler is free to interleave them. Reads are blue, writes orange, and the Value column is the variable right after each step. Under Strict 2PL every lock is held until COMMIT or ABORT, which is what keeps this interleaving safe.
Notes ↗
tm-three-transactions · Scenario 1: All Transactions Commit

Scenario 1: all three transactions commit

Scenario 1: all three transactions commit A write-ahead log table for the all-commit run: each row is a logged write or a commit record, in order, with the old and new value of A, B, or C. T2 commits, then T1 commits, then T3 commits; every write is logged before the commit record. The final database state is A=27.36, B=38.4, C=32.4, all durable. The violet D circle marks Durability: once a commit record reaches the WAL, that transaction's changes survive a crash. Scenario 1: all three commit Each change is logged to the WAL before COMMIT, so on commit the change is already durable. Write-Ahead Log Txn Record Variable Old → New T2 W A 19 → 22.8 T2 COMMIT A is now durable T3 W B 32 → 38.4 T1 W A 22.8 → 27.36 T1 COMMIT A is now durable T3 W C 27 → 32.4 T3 COMMIT B and C now durable D Durability Three commit records reached the WAL, so all three changes survive a crash. Final state: A = 27.36, B = 38.4, C = 32.4 Color key: W = a logged write · COMMIT = the change is durable · D = Durability
Scenario 1: all three commit. Each write is logged to the WAL, then a COMMIT record follows, so the change is durable the moment the commit lands. Strict 2PL holds each lock until that COMMIT, so no transaction ever read another's uncommitted data. The database advances to one new consistent state: A=27.36, B=38.4, C=32.4, all durable (the violet D for Durability).
Notes ↗
tm-three-transactions · Scenario 2: Mixed Outcomes, T3 Aborts

Scenario 2: T3 aborts while T1 and T2 commit, with no cascade

Scenario 2: T3 aborts while T1 and T2 commit, with no cascade A write-ahead log table for the run where T3 aborts: T2 and T1 log their writes to A and commit, then T3's writes to B and C are undone by reading the WAL backward, reverting C from 32.4 to 27 and B from 38.4 to 32, and an abort record is written. T1 and T2 are not rolled back because Strict 2PL kept T3's locks held, so neither ever read T3's uncommitted data. The final database state is A=27.36, B=32, C=27. The blue A circle marks Atomicity: T3 leaves no trace. Scenario 2: T3 aborts, T1 and T2 commit T3's locks were held the whole time, so no one read its dirty data. One abort, no cascade. Write-Ahead Log Txn Record Variable Old → New T2 W A 19 → 22.8 T3 W B 32 → 38.4 T2 COMMIT A = 22.8 is durable T3 W C 27 → 32.4 T1 W A 22.8 → 27.36 T3 UNDO C 32.4 → 27 T3 UNDO B 38.4 → 32 T3 ABORT B, C restored; T3 leaves no trace T1 COMMIT A = 27.36 is durable A Atomicity, and no cascade T3's writes are undone in full, so it is all-or-nothing. T1 and T2 never saw T3's uncommitted data (its locks were held), so neither rolls back. One abort, contained. Final state: A = 27.36, B = 32, C = 27 Color key: W = a logged write · COMMIT = durable · UNDO = reverted on abort · A = Atomicity
Scenario 2: T3 aborts while T1 and T2 commit. The recovery manager reads T3's WAL entries backward and restores the old values, reverting C from 32.4 to 27 and B from 38.4 to 32, so T3 leaves no trace (the blue A for Atomicity). The final state is A=27.36, B=32, C=27.
Notes ↗
M4

Problem Solving: The Taylor Swift Ticket Disaster

A correct transaction manager still falls over under real load.

4 figures
problem-solving · The Core Problems

Ticketdisaster.com checkout flow under Eras Tour load

Ticketdisaster.com checkout flow under Eras Tour load A five-step flow read left to right. Virtual Queue holds no lock, grey. Zone Selection locks 1000-plus seats and starts a 5-minute timer, amber. Seat Browsing keeps the lock held, amber. Payment Entry runs the timer critical, red. Timeout releases the lock and kicks the user out, red. A dashed loop sends the user back to the 2-hour queue. Color key: grey = no lock yet, out of focus; amber = lock acquired and held; red = the failure, lock lost. Ticketdisaster.com user flow The 5-minute lock timer starts the moment a zone is picked, and the lock is held through payment Virtual Queue Wait 2 to 3 hours behind 50k+ users no lock yet timer not started Zone Selection User picks a zone (e.g. Zone 5) LOCK · 1000+ seats timer starts at 5:00 Seat Browsing User scans the map, picks 4 seats LOCK still held timer 4:30 to 2:00 Payment Entry User enters card, billing, confirms LOCK · timer critical timer 2:00 to 0:30 TIMEOUT Payment failed lock released user kicked out Restart cycle, back to the 2-hour queue Color key grey = no lock yet · amber = lock acquired and held · red = the failure, lock lost
The Ticketdisaster.com checkout flow under Eras Tour load. The lock acquires the instant a user picks a zone and stays held through browsing and payment, so one slow user freezes a thousand seats for minutes. The three worst bottlenecks come next.
Notes ↗
end of lecture 2~70 min of figures
problem-solving · The Smart Solutions: Ticketsfaster.com

Fix two: shrink the lock

Fix two: shrink the lock Before, red: locking a whole zone freezes a thousand seats, so nine hundred ninety-nine other fans are blocked and only about a hundred users run at once. After, green: locking a single seat blocks nobody else, so about ten thousand users browse different seats at once. Color key: red is the old flow, green is the fixed flow. Fix 1: shrink the lock Before: lock the zone One pick freezes the whole zone. 1,000 seats locked 999 fans blocked Concurrent users: ~100 After: lock one seat Everyone else browses freely. 1 seat locked 0 fans blocked Concurrent users: ~10,000 red = the old flow · green = the fix
Shrink the lock. A zone lock freezes a thousand seats, so nine hundred ninety-nine other fans wait on one. A seat-level lock blocks nobody else, and about a hundred times more fans browse at once.
Notes ↗
problem-solving · The Smart Solutions: Ticketsfaster.com

Fix one: cut lock duration

Fix one: cut lock duration Before, red: the app locks the whole zone and holds it through browsing and payment, five to ten minutes, so only about a hundred users run at once. After, green: the fan previews and pre-fills while waiting with no lock, then locks only for the final write, thirty seconds, so about ten thousand users run at once. Color key: red is the old flow, green is the fixed flow. Fix 2: cut lock duration Before Lock the whole zone, then browse and pay with the lock held. Lock held: 5 to 10 minutes Concurrent users: ~100 After Pre-fill card and address in the queue. Lock only for the final write. Lock held: 30 seconds Concurrent users: ~10,000 red = the old flow · green = the fix
Cut lock duration. The old flow holds the zone lock through browsing and payment, five to ten minutes, so only about a hundred fans run at once. The fix does the slow steps in the queue with no lock and takes the lock only for the final write, about thirty seconds, so about ten thousand fans run at once.
Notes ↗
problem-solving · The Smart Solutions: Ticketsfaster.com

Fix three: fall back to async

Fix three: fall back to async Before, red: fans block in real time, holding locks through hours-long queues, so only about a hundred users run at once. After, green: fans submit instantly with a timestamp, a background system processes them in order and emails the confirmation, so about fifty thousand users run at once. Color key: red is the old flow, green is the fixed flow. Fix 3: fall back to async Before: block in real time Wait in the queue holding a lock the whole time. 3-hour queue waits Concurrent users: ~100 After: stamp and queue Submit instantly. A background job processes it and emails you. Instant submit, email confirm Concurrent users: ~50,000 red = the old flow · green = the fix
Fall back to async. Instead of blocking in real time, fans submit instantly with a timestamp, a background job processes them in order, and the confirmation arrives by email. The three-hour queue becomes a sixty-second submit.
Notes ↗
M4

Where's the Next 100x?

Strict 2PL gives safe, correct concurrency and runs most production databases today, but it refuses many schedules that are perfectly serializable.

4 figures
next-100x

Where the next 100x comes from: not a faster Strict 2PL, but systems that drop it

Where the next 100x comes from: not a faster Strict 2PL, but systems that drop it A scaling axis runs left to right: about 100 times per decade, about 10,000 times per generation. Two paths fork from where databases run today. The grey path, faster Strict 2PL, is a dead end. The violet path, systems that drop Strict 2PL entirely, is where the next 100 times leap lives. Color key: violet = the path that scales, the systems that drop Strict 2PL; grey = the dead end, a faster Strict 2PL; blue = where production databases run today. Where does the next 100× come from? About 100× per decade, about 10,000× per generation. The leap is not a faster Strict 2PL. SCALE ~100× / decade  ·  ~10,000× / generation Where we run today Strict 2PL: safe, correct concurrency. It scales to most production databases. The baseline modern systems build beyond. Drop Strict 2PL entirely New protocols admit schedules 2PL refuses. Micro-locking. LSM trees. Timestamp order. This is where the next 100× lives. Just make Strict 2PL faster A dead end. The same protocol, same ceiling. Color key  violet = the path that scales  ·  blue = where databases run today  ·  grey = the dead end
Scaling runs about 100x per decade. Strict 2PL, blue, is where production databases run today. The next 100x, violet, comes from protocols that drop Strict 2PL and admit the schedules it refuses; tuning 2PL itself, grey, does not scale.
Notes ↗
next-100x · The Opportunity Space

Nested hierarchy: every S2PL schedule is a 2PL schedule, every 2PL schedule is conflict-serializable, every conflict-serializable schedule is serializable

Nested hierarchy: every S2PL schedule is a 2PL schedule, every 2PL schedule is conflict-serializable, every conflict-serializable schedule is serializable Nested hierarchy: every S2PL schedule is a 2PL schedule, every 2PL schedule is conflict-serializable, every conflict-serializable schedule is serializable Serializable schedules Conflict-Serializable (CS) 2PL S2PL S2PL ⊂ 2PL ⊂ Conflict-Serializable ⊂ Serializable
Every ring outside S2PL is safe, valid schedules that today's S2PL databases refuse to run. The Serializable − CS ring and the CS − 2PL ring are real schedules a smarter scheduler could admit; that untapped gap is the 100x opportunity.
Notes ↗
next-100x · Match the Protocol to the Workload

Choosing a path by workload signature: each signature maps to one protocol, the real systems that use it, and a case study at scale

Choosing a path by workload signature: each signature maps to one protocol, the real systems that use it, and a case study at scale Three rows, each reading left to right from a workload signature, to a protocol, to the real systems and a case study. Row one: fine-grained contention maps to micro-locking, used by Postgres, SQL Server, Oracle; the case is a video going viral with ten million users hitting one counter. Row two: write throughput maps to LSM trees and columnar stores, used by ClickHouse, BigQuery, Snowflake; the case is a song landing ten million plays in an hour. Row three: global ordering maps to timestamp ordering, used by Spanner; the case is ten thousand agents updating one shared graph. Color key: violet = the protocol chosen for each signature; blue = the real systems that ship it; grey = the workload signature you read off the system. The workload picks the protocol Read the signature off the system, and the protocol falls out. Same theory underneath, different operating point. WORKLOAD SIGNATURE PROTOCOL SHIPS IN / CASE STUDY Fine-grained contention High-value transactions where every commit must be safe. TikTok: 10M users, one counter. Micro-locking Field-level locks, smart deadlock detection. Maximum concurrency with safety guarantees. Postgres · SQL Server · Oracle Lock only the cell that is hot. Write throughput Analytics: ingest billions of events, read them back later. Spotify: 10M plays in an hour. LSM trees / columnar Append-only writes, background compaction. Eliminate lock contention entirely. ClickHouse · BigQuery · Snowflake Writes that never conflict. Global ordering Distributed systems where global order is the hard part. 10K agents, one shared graph. Timestamp ordering Global ordering without distributed locks. First timestamp wins, late agents retry. Spanner No deadlocks, deterministic results. Color key  violet = the protocol chosen  ·  blue = the real systems that ship it  ·  grey = the workload signature
Each workload signature, grey, maps to one protocol, violet, and the real systems that ship it, blue. The signature is the case: a viral counter, a play stream, a shared graph.
Notes ↗
next-100x · The Next Machine

The takeaway: Strict 2PL is the baseline, the next 100x lives in the schedules it refuses, the same correctness math holds, and next the machine splits into many

The takeaway: Strict 2PL is the baseline, the next 100x lives in the schedules it refuses, the same correctness math holds, and next the machine splits into many Three stacked statements. First, Strict 2PL is the correctness baseline, and the next 100 times leap lives in the schedules it refuses to admit. Second, correct still means serializable; conflict-serializability is only the test 2PL passes, so a smarter scheduler can admit even the serializable-but-not-conflict-serializable schedules. Third, every protocol here was one database on one machine, and next we break that machine into a thousand, which is distributed systems. The third row is the ink takeaway pointing forward. Color key: violet = the protocols beyond Strict 2PL; green = the correctness math that still holds; ink = the takeaway, where we go next. The next 100× lives in the schedules Strict 2PL refuses Strict 2PL is the baseline of database concurrency The correctness baseline modern systems build beyond. The leap lives in the safe schedules it refuses to admit, the ones a smarter protocol can run. Correct still means serializable Conflict-serializability is only the test 2PL passes; a smarter scheduler can admit more. Every protocol here was one database on one machine. Next we break that machine into a thousand: the data, the locks, and the clock on different computers. Color key  violet = the protocols beyond Strict 2PL  ·  green = the correctness math that still holds  ·  ink = where we go next
Strict 2PL, violet, is the baseline, and the next 100x lives in the safe schedules it refuses. What defines correct is serializability, green: a smarter scheduler can admit schedules that are serializable yet not even conflict-serializable. Every protocol here ran on one machine; distributed systems break that machine into a thousand, with the data, the locks, and the clock on different computers.
Notes ↗
end of lecture 3~10 min of figures
M4 Transactions, recovery, and the whole manager 1 of 78
Notes ↗ Video ▶ Why ◎ Schedule ↗ □ keys

M4 Transactions, recovery, and the whole manager

Correctness under concurrency, then surviving a crash · 78 figures · ~148 min · 3 lectures at this pace