M4

Transactions Motivation

A transaction groups related database operations into one all-or-nothing unit.

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. 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
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 ↗

Taylor Swift’s Eras Tour on sale

Taylor Swift’s Eras Tour on sale Four figures from the on-sale: 52 show dates, 3.5 million fans pre-registered, over 2 million tickets sold in a single day, and 3.5 billion system requests in a few hours. 52 show dates 3.5M fans pre-registered 2M+ tickets sold in a single day 3.5B system requests in a few hours
Ticketmaster reported 3.5 million registrations and 3.5 billion system requests, four times its prior traffic peak. Sales were delayed and fans encountered errors. The transaction guarantees are simple: no seat sold twice and no charge without a seat. Keeping those guarantees while staying responsive under load is the problem this module addresses.
Notes ↗

Heavy demand tests availability and correctness

Heavy demand tests availability and correctness Ticketmaster reported 3.5 million registrations, 3.5 billion requests, and four times its previous traffic peak. Queues and purchase flows were disrupted. The required guarantees are no duplicate seat sale and no completed charge without a seat; these are requirements, not an audit of every purchase. Reported demand 3.5M registrations 3.5B requests 4× the prior traffic peak Availability suffered Long waits and errors Sales slowed or postponed Fans lost carted tickets Required guarantees No seat sold twice No charge without a seat All or nothing
Ticketmaster reported 3.5 million registrations and 3.5 billion system requests, four times its prior traffic peak. Sales were delayed and fans encountered errors. The transaction guarantees are simple: no seat sold twice and no charge without a seat. Keeping those guarantees while staying responsive under load is the problem this module addresses.
Notes ↗

Large workloads need deliberate engineering

Large workloads need deliberate engineering Visa reports capacity above 65,000 transaction messages per second. During Prime Day 2022 DynamoDB peaked at 105.2 million requests per second with single-digit millisecond responses. Ticketmaster reported four times its prior traffic peak. The metrics describe different workloads. Visa network 65,000+ transaction messages/sec capacity Payment network Amazon DynamoDB · Prime Day 2022 105.2M requests/sec · single-digit ms Database requests Ticketmaster · Eras Tour presale 4× its previous traffic peak Availability disrupted
Visa reports capacity above 65,000 transaction messages per second. During Prime Day 2022, Amazon DynamoDB peaked at 105.2 million requests per second with single-digit millisecond responses. Ticketmaster reported four times its previous traffic peak. These are different workloads and metrics, but all require deliberate engineering for concurrency and availability.
Notes ↗

More writes, the same correctness guarantees

More writes, the same correctness guarantees Three illustrative workload tiers. Classic retail systems such as Walmart in the 1990s and 2000s illustrate about ten thousand writes per second. Cloud services such as Gmail and Roblox in the 2010s and 2020s illustrate millions of writes per second. Agent workloads from 2025 onward motivate a target of tens of millions of writes per second with NVMe and distributed compute. These rates are illustrative tiers, not measured throughput attributed to the named services. Classic databases 1990s–2000s Retail · Walmart Illustrative write throughput ~10,000 writes/sec Hardware HDDs · a few CPU cores Data structures B-trees Cloud services 2010s–2020s Gmail · Roblox Illustrative write throughput Millions of writes/sec Hardware SSDs · many CPU cores Data structures B-trees + LSM trees Agent workloads 2025 onward Many autonomous clients Illustrative write throughput Tens of millions/sec Hardware NVMe · distributed compute Data structures Workload-specific indexes
Classic retail systems such as Walmart, cloud services such as Gmail and Roblox, and agent workloads from 2025 onward illustrate increasing write demand: about 10,000 writes per second, millions, and a target of tens of millions. These are illustrative scale tiers, not measured rates for the named services. Hardware progresses from disks and a few cores to SSDs and many cores, then NVMe with distributed compute; every tier still needs correct transactions.
Notes ↗
M4

ACID Properties

ACID defines four guarantees for a transaction.

Alice’s ticket purchase: commit or rollback

Alice’s ticket purchase: commit or rollback One purchase checks inventory, authorizes the card, and records the sale. Success commits the database changes. A declined authorization rolls back pending database changes; no completed charge or ticket remains. External payment systems require coordinated application logic. BEGIN Check inventory Seats available Authorize card Approved Record sale Sale recorded COMMIT Tickets confirmed BEGIN Check inventory Pending changes undone Authorize card Card declined Record sale Not executed ROLLBACK No ticket · no completed charge
Alice’s purchase checks inventory, requests card authorization, and records the sale. The database commits its pending changes together or rolls them back on failure. A declined authorization leaves no confirmed sale or completed charge. Coordination with the external payment network is application logic; a database rollback alone cannot reverse an external charge.
Notes ↗

ACID: four guarantees, four examples

ACID: four guarantees, four examples Four cards use blue A, green C, orange I, and violet D badges to distinguish each ACID definition from its ticket-purchase example: atomicity commits all writes or none, consistency preserves rules, isolation hides intermediate work, and durability retains committed data after a crash. A Atomicity All writes commit together, or none do. Example Card declines: no ticket, no charge, no change. C Consistency Every commit preserves the rules. Example Seats sold and revenue stay in sync. I Isolation Transactions hide their unfinished work. Example Alice locks the last seat; Bob waits. D Durability Committed changes survive a crash. Example Server crashes after confirmation. The ticket remains recorded.
Each card separates a guarantee from its ticket-purchase example: 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 ↗

Interest must not see a half-finished transfer

Interest must not see a half-finished transfer T1 moves $100 from Alice to Bob. T2 adds 6% interest to both accounts. Time runs down two schedules: without isolation T2 reads the unfinished transfer and the final total is $324; with isolation T2 waits for T1 to commit and the total is $318. Red marks excess interest; green marks the correct result. T1 · Move $100 Alice → Bob T2 · Add 6% interest To both account balances Start: Alice $200 · Bob $100 · total $300 Without isolation Time ↓ T1 · Move T2 · Interest Bob +$100 Alice $200 Bob $200 Reads $200 + $200 Adds 6% on $400 Both become $212 Alice −$100 Commit $324 $6 extra Alice $112 · Bob $212 With isolation Time ↓ T1 · Move T2 · Interest Bob +$100 Waits for T1 Alice −$100 Commit Reads $100 + $200 Adds 6% on $300 After T1 commits $318 Correct interest Alice $106 · Bob $212
T1 moves $100 from Alice to Bob; T2 adds 6% interest to both accounts. Read each schedule downward, with a separate lane for each transaction. Without isolation, T1 credits Bob before debiting Alice, so T2 sees $400 and adds $24 of interest; T1 then finishes, leaving $324 (red), including $6 in excess interest. With isolation, T2 waits for T1 to commit and sees the settled $300, adding the correct $18 and leaving $318 (green).
Notes ↗
M4

How ACID Protects a Stripe Payment

One payment updates several records that must agree.

One transaction commits four internal payment writes

One transaction commits four internal payment writes After card authorization, a payment service records a payment, writes balanced ledger entries, updates the merchant balance, and queues a webhook in one illustrative database transaction. Commit all four internal writes or roll them all back. Pay $9.99 Card authorized Record the payment Store the authorized payment Write the ledger Two entries that balance to zero Update merchant balance Add $9.99 to the balance Queue the webhook Persist the notification to send Commit all four or roll back all four.
After card authorization, the database records the payment, writes two balancing ledger rows, updates the merchant balance, and queues a webhook. One transaction commits those four internal writes together.
Notes ↗

Four ACID properties prevent four payment failures

Four ACID properties prevent four payment failures A two-by-two grid of failures. Atomicity prevents a payment record from committing without its webhook entry. Consistency prevents ledger rows from falling out of balance. Isolation prevents concurrent payments from losing a balance update. Durability prevents a confirmed payment from vanishing after a crash. Color key: the red bar marks each failure, and the coloured letter marks the ACID property that prevents it. The partial internal state Payment recorded. Webhook missing. The database commits only half the work. 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
Four failures map to four guarantees: atomicity prevents partial internal writes, consistency keeps the ledger balanced, isolation prevents concurrent payments from losing a balance update, and durability preserves a confirmed payment after a crash.
Notes ↗
M4

Sample Code: Running Transactions

Use BEGIN and COMMIT to group statements into one transaction.

BEGIN, COMMIT, ROLLBACK

BEGIN, COMMIT, ROLLBACK BEGIN starts a database transaction. COMMIT persists its changes; ROLLBACK discards its pending changes. Outside an explicit block, autocommit treats each statement as a transaction. Green marks commit and red marks rollback. BEGIN TRANSACTION UPDATE inventory … INSERT INTO orders … Related writes, one transaction COMMIT Persist the transaction’s changes. ROLLBACK Discard pending changes. Autocommit Outside the block, each statement is its own transaction.
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 ↗

Buy tickets

BEGIN TRANSACTION;

SELECT available_tickets
FROM Concerts
WHERE concert_id = 'taylor_swift_123';

UPDATE Concerts
SET available_tickets = available_tickets - 2
WHERE concert_id = 'taylor_swift_123';

INSERT INTO Orders (user_id, concert_id, num_tickets, total_amount, status)
VALUES ('user_789', 'taylor_swift_123', 2, 150.00, 'confirmed');

COMMIT;
Notes ↗

Transfer money

-- Schema rule: CHECK (balance >= 0).
-- The database enforces this on every write.

BEGIN TRANSACTION;

UPDATE Accounts
SET balance = balance - 100.00
WHERE account_id = 'alice_account';

UPDATE Accounts
SET balance = balance + 100.00
WHERE account_id = 'bob_account';

INSERT INTO Transfers (from_account, to_account, amount, type)
VALUES ('alice_account', 'bob_account', 100.00, 'transfer');

COMMIT;
Notes ↗

Reserve inventory

BEGIN TRANSACTION;

SELECT quantity FROM Products WHERE product_id = 'labubu';

UPDATE Products SET quantity = quantity - 1 WHERE product_id = 'labubu';

INSERT INTO Orders (customer_id, product_id, quantity, status)
VALUES ('customer_alice', 'labubu', 1, 'confirmed');

COMMIT;
Notes ↗

Record a like

BEGIN TRANSACTION;

-- The Likes table has a UNIQUE (user_id, post_id) constraint; a duplicate
-- INSERT fails and the whole transaction rolls back.
INSERT INTO Likes (user_id, post_id, timestamp)
VALUES ('user_12345', 'viral_post_456', NOW());

UPDATE Posts
SET like_count = like_count + 1,
    last_activity = NOW()
WHERE post_id = 'viral_post_456';

COMMIT;
Notes ↗
M4

Naive Concurrency

Concurrency control keeps shared data correct while transactions run together.

I/O lag: reads and writes take time

I/O lag: reads and writes take time Two timelines show read start RS to read end RE, and write start WS to write end WE. Each start-to-end interval takes five time units in this example. Thin vertical lines mark the events and dashed spans show the wait. In practice, I/O can take millions of CPU cycles, depending on the device and operation. Read Write RS Read start WS Write start RE Read end 5 time units in this example WE Write end 5 time units in this example I/O lag = end time − start time
I/O lag is the time from read start (RS) to read end (RE), or write start (WS) to write end (WE). Here it takes 5 time units. In practice, I/O can take millions of CPU cycles.
Notes ↗

Naive Way 1: run one at a time

Naive Way 1: run one at a time A server runs T1 and then T2. Short white compute blocks alternate with much longer gray disk read/write waits. The storage device handles the I/O while serial scheduling leaves the CPU idle instead of running another transaction. Broken arrows indicate compressed waiting time, not a measured duration. T3 and T4 cannot run during T2’s wait. Server Time → T1 Run Disk R/W · CPU idle Wait continues… T2 Run Disk R/W · CPU idle Wait continues… T3 Run T4 … Next T3 and T4 wait behind T2, even while the CPU is idle.
The disk handles reads and writes while serial execution leaves the CPU idle. The broken arrows compress the waiting spans; they do not represent measured durations. T3 and T4 cannot run while T2 waits, even when their work touches unrelated rows.
Notes ↗

Naive Way 2: run without coordination

Naive Way 2: run without coordination 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. 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
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 ↗

How should transactions share the database?

How should transactions share the database?Three approaches: serial execution, uncoordinated execution, and concurrency control. Direct labels replace a safety-versus-performance axis.SerialRun one transaction at a time.Disk waits leave the CPU idle.UncoordinatedRun transactions without checks.Conflicting writes can lose updates.Concurrency controlOverlap independent work.Order conflicting operations safely.
Concurrency control overlaps independent work while ordering conflicting actions safely.
Notes ↗
M4

A Quick Graph Refresher

A directed graph captures which operations must come before others.

Acyclic graphs have no directed cycle

Acyclic graphs have no directed cycle Two neutral diagrams compare an acyclic chain T1 to T2 to T3 with a cycle T1 to T2 to T3 to T1. Arrowheads show direction; color is not needed to distinguish them. Acyclic (DAG) T1 T2 T3 An order respects every edge. Cyclic T1 T2 T3 T1 → T2 → T3 → T1
Left: no directed cycle, so an order respecting every edge exists. Right: the cycle prevents such an order. A transaction schedule is conflict-serializable exactly when its conflict graph has no cycle.
Notes ↗

A topological order respects every edge

A topological order respects every edge The DAG has edges A to B, A to C, B to D, and C to D. Its two topological orders are A B C D and A C B D. B and C have no ordering constraint between them. Dependencies A B C D Two valid orders A → B → C → D A → C → B → D B and C can appear in either order.
Both A → B → C → D and A → C → B → D respect every edge. B and C have no ordering constraint between them. In a conflict graph, a topological order gives an equivalent serial transaction order.
Notes ↗
M4

Correctness: Ensuring Safe Concurrent Execution

A correct concurrent schedule produces the result of a valid serial order.

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 labels are writes. Correctness tracks only the order of these actions, not the values they compute. 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 labels are reads, orange labels are writes. Correctness tracks only their order, not the values they compute.
Notes ↗

Three kinds of schedule

Serial, interleaved, and serializable schedules Three labeled rows, each a schedule for T1 and T2 drawn as four action boxes left to right in time order, each box written like T1.R(A), blue for a read and orange for a write. The rows are Serial, Interleaved and Serializable. The Serializable row carries a second run of boxes after an equivalence sign, labeled a serial run. Color key: blue read, orange write. 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
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 ↗

When do actions conflict?

When do actions conflict? 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 ↗

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. 1 · ONE NODE PER TRANSACTION T1 T3 T2 2 · each conflict is a directed edge 3 · LOOK FOR A CYCLE ✓ No cycle → conflict-serializable Topological order gives a serial order. ✗ A cycle → not conflict-serializable A cycle demands incompatible orders.
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 ↗

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. Each transaction runs as one contiguous block, bracketed and labeled on the left, so the 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
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 ↗

A serial schedule has no conflict cycle

A serial schedule has no conflict cycle 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 ↗

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. The transactions interleave, so no blocks are bracketed. 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 ↗

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 ↗

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. The transactions interleave, so no blocks are bracketed. 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. Its final values happen to match the serial run, so the totals are not the tell: the cycle is.
Notes ↗

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.
Notes ↗

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. 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
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 ↗

How schedule classes fit together

How schedule classes fit together 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
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

SQL, database locks, OS mutexes, and hardware atomics coordinate concurrent work.

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. 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)
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 ↗

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. 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.
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 ↗

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. 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 transaction requests a lock, uses the protected row, and releases the lock.

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. 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 ↗

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. 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 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. 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

Two-phase locking acquires locks before releasing any of them.

end of lecture 1~69 min of figures

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 ↗

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
Nothing is released early.
Notes ↗

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. 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

A microschedule shows how transactions interleave operations and acquire locks.

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. T1 Req(A) T2 Req(A) T3 Req(B) LOCK MANAGER · THE LOCK TABLE Row A held by T1 (X) · waiting: T2 Row B free → granted to T3 1 Grant or queue Checks lock compatibility, then grants the request or queues the transaction. 2 Builds the schedule Records grants and waits is the microschedule: the 3 Detects deadlock Tracks the WaitsFor graph. Aborts a victim to break the deadlock.
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 ↗

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. 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 ↗

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 ↗

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 ↗

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 ↗

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 18 19 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 We(B) 17 Req(A) 18 Per-Transaction Locks T1 A: {"Req": 1, "Get": 2} B: {"Req": 15} T2 A: {"Req": 18} 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": 18} A: {"Req": 1, "Get": 2} T1 holds A T2 waits A (18, _) WaitsFor cycle T1 ↔ T2. 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)

MVCC lets readers use earlier row versions while writers create new ones.

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. 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 ↗

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. Value V time → AB1 · 2:00 starts 2:45 Minnie writing XY34 (uncommitted) commits 3:03 reads Pluto · snapshot 2:50 AB1 reads Mickey · snapshot 3:00 AB1 XY34 · 3:03 reads Query · snapshot 3:05 XY34
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 ↗

Vacuum: delete versions no 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. 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
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 ↗

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. 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
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 runs first, validates at commit, and retries conflicts.

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. 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. Optimistic execution avoids waiting, but retries when validation finds a conflict.
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

Concurrency control combines a correctness standard with an execution strategy.

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. 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
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

Log changes instead of copying the whole database.

The version problem: keep the past without copying it

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. The log shows three entries: at 10:31:04, account 12 cash goes from 5000 to 4820; at 10:31:04, account 88 cash goes from 210 to 640; at 10:31:05, account 12 shares go from 3 to 4. To know account 12 at 10:31:04, replay the entries up to then. 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. 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.
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 ↗

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. 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.
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 ↗

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 run left to right along a time axis: a1b2, plus schema.sql, by Mia, Mon; c3d4, about four lines changed, by Theo, Tue; e5f6, minus dead code, by Mia, Wed. Each carries the note shares unchanged files. A green arrow runs left to right above the row, labeled replay forward to redo committed work, and a red arrow runs back below it, labeled replay backward to undo it. Color key: violet the commits, each with its author and time, green forward to redo, red backward to undo. 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
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

Write-ahead logging records each change before its data page reaches disk.

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 The update, value A going from 19 to 22.8, sits at the left. Two grey arrows leave it: one up to the violet write-ahead log panel, one down to the red RAM buffer, drawn as a grid of 64 MB page slots with A's page marked dirty. A green arrow then crosses to the disk panel on the right, the same grid of durable slots, 19 of 2048 pages. A time axis along the bottom places steps one and two together at now and step three far to the right at later. Color key: violet is the durable sequential log, red is the volatile RAM buffer, green is the durable disk, grey is page structure. 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.
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 ↗

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. Txn Var old value new value … … … … T1 A 19 22.8 T1 B 23 27.6 T1 COMMIT record — T1 ends here Append each change; finish with a commit record. 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 ↗

The log is the version history

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. 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
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 ↗

Small and sequential, not big and random

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. 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. 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 ↗

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. 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.
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

Undo reverses an aborted transaction using the old values in its log.

Undo is a write too: it appends W-abort rows

Undo is logged: it appends W-abort rows to the log A five-column log table, Txn, Action, Var, old value, new value, built in three stages. First T1's two original writes. Then, under a note that T1 aborts, the two W-abort rows undo appends, drawn in red. Last the abort record, its value columns dashes. Color key: orange is the old value, green is the new value, red marks undo's own 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.
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 ↗

The log: TLong aborts and unwinds

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. 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. 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

Recovery preserves committed changes and removes unfinished work after a crash.

A crash leaves one question: which work survives?

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. 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
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 ↗

Two passes over one log: replay down, then undo up

Crash recovery on one WAL: a forward replay rebuilds the crash state, then a backward undo reverses the unfinished 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. 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: recovery begins Replay the durable log, then undo unfinished transactions. 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 unfinished (newest-first) final A=12 B=20 C=30 D=48 E=50 T1, T4 committed. T3, T5 undone. T2 had already aborted.
Recovery as two passes over one log:
Notes ↗

Just after the crash: the disk holds a half-written mix

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. 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 untouched by the crash 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.
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 restores durability and atomicity across a crash

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. 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.
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

The transaction manager coordinates concurrency, logging, and recovery.

Five components of a transaction manager

Five components of a transaction managerFive neutral rows show scheduling, locking, logging, deadlock detection and recovery. These cooperate; ACID properties do not map one-to-one to components.Transactions: BEGIN → reads and writes → COMMIT or ABORT1. SchedulerCoordinates the interleaving of transaction operations.2. Lock managerGrants compatible locks; queues conflicting requests.3. Write-ahead logMakes log records durable before modified pages reach disk.4. Deadlock detectorFinds waits-for cycles and aborts a victim.5. Recovery managerReplays the log, then undoes unfinished transactions.Together: isolated transactions and recoverable updates.
Scheduling and locking provide isolation. Logging and recovery support atomicity and durability; deadlock detection breaks cycles of waiting. Application logic and constraints preserve consistency.
Notes ↗

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. 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
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 ↗

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 An example workload has mostly independent transactions and a small group that conflicts. Hash-based lock checks and sequential logging reduce overhead. Actual throughput depends on the workload and hardware. ~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.
In this example, most transactions access different rows. Actual contention depends on the workload; hash-based lock checks and sequential logging reduce overhead.
Notes ↗
M4

Three Transactions, Three Variables

Trace three transactions through scheduling, locking, logging, commit, and abort.

One interleaved schedule, three transactions

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. 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 ↗

Scenario 1: all three 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. 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
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 ↗
end of lecture 2~69 min of figures

Scenario 2: T3 aborts, T1 and T2 commit

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. 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
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

Correct transactions still need an application design that handles heavy demand.

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. 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
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 ↗

Fix 1: 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. Before: lock the zone 1,000 seats locked 999 fans blocked Concurrent users: ~100 After: lock one seat 1 seat locked 0 fans blocked Concurrent users: ~10,000
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 ↗

Fix 2: 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. Before Lock the whole zone, then browse and pay with the lock held. Lock held: 5 to 10 minutes After Pre-fill card and address in the queue. Lock only for the final write. Lock held: 30 seconds
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 ↗

Fix 3: 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. Before: block in real time Wait in the queue holding a lock the whole time. 3-hour queue waits After: stamp and queue Submit instantly. A background job processes it and emails you. Instant submit, email confirm
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?

Choose the next optimization by measuring contention, I/O, and coordination costs.

Where could the next 100× come from?

Where could the next 100× come from?Three separately revealed design choices. Performance depends on the workload; no fixed speedup is guaranteed.Reduce contentionLock a smaller unit or hold locks for less time.Batch writesUse buffered writes and compaction for event streams.Change concurrency controlChoose locking, versions, or validation to fit the workload.
Choose the change that addresses the measured bottleneck. Improving Strict 2PL and choosing another protocol are both options.
Notes ↗

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
Serializability is the correctness target. A scheduling rule may be more restrictive to make correctness practical to enforce.
Notes ↗

Match the workload to the mechanism

Match the workload to the mechanismThree neutral rows compare finer-grained locks, LSM storage and timestamp ordering. Violet identifies the approach. These are mechanisms that can be combined, not interchangeable guarantees.WORKLOADAPPROACHTRADE-OFFIndependent fieldsFiner-grained locksLock only the field being updated.More lock bookkeeping.One hot field still serializes.Many incoming eventsLSM treesBuffer writes, flush, then compact.Read/write amplification.Transactions still need coordination.Ordering conflictsTimestamp orderingReject operations that violate the order.Aborted transactions must retry.
These mechanisms solve different problems. LSM storage does not replace transaction concurrency control.
Notes ↗

Scale the workload. Preserve correctness.

Scale the workload. Preserve correctness.Three separately revealed design choices. Performance depends on the workload; no fixed speedup is guaranteed.Measure the bottleneckContention, storage I/O, or coordination?Choose the mechanismFiner locks, buffered writes, or a different concurrency protocol.Then distribute the workAcross machines, communication and failures become part of the design.
Distributed systems add communication and partial failures to the same correctness and performance questions.
Notes ↗
end of lecture 3~15 min of figures
M4 Transactions, recovery, and the whole manager 1 of 78
▶ Y Notes □ keys

M4 Transactions, recovery, and the whole manager

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