Chapter Six · MVCC and Isolation

MVCC and Isolation

Two customers were both sold the last box of strawberries, and nothing errored. Six topics explain why that is the documented behaviour of the default isolation level, fix it three ways with three different costs, and show what an open transaction costs the rest of the cluster.

6 topics

Chapter 5 opened the file and showed what a row physically is: a header, a set of column values, a line pointer, and after an update a second complete copy of the same row sitting on the page beside the first. This chapter is about the other half of that picture — which of those copies a given transaction is allowed to see, and who decides.

The answer is a snapshot, and it is small: three numbers that every row version on every page is judged against. Everything people find surprising about concurrency in Postgres falls out of that structure and of when it is taken. Two identical queries in one transaction returning different counts, a report that disagrees with the dashboard, an UPDATE that affects zero rows, a serialization failure at three in the morning, and an idle connection that stops a 900 MB table from ever shrinking — five symptoms, one mechanism.

One of Cartwheel's two open wounds closes here. On a Saturday in March, two checkouts read on_hand = 1 for the same box of strawberries, both decided the sale was fine, and both wrote zero. Topic 30 fixes it three ways: a single guarded statement at the default isolation level, an explicit row lock, and Serializable with a retry loop — with a recommendation and the reasoning behind it, rather than three options and a shrug. The Saturday-morning slowdown stays open; that one is a planner problem and belongs to Chapter 9.

Five symptoms, one mechanism
A transaction writesand is handed an id
Every row version carries xmin and xmax
Every query holds a snapshotthree numbers
Each version is judged against itthe sole arbiter
The oldest snapshot pins cleanupcluster-wide

Topics in This Chapter

Topic 27
The Problem MVCC Solves
Readers never block writers, writers never block readers, and two writers of the same row still take turns. The trade that buys those properties, the dead rows it produces, and the one class of bug it deliberately does not prevent.
Concurrency Model
Topic 28
xmin, xmax, and a Row's Lifetime
The transaction ids stamped on every row version, selectable from SQL in any session. Why a read-only query consumes no id at all, what the commit log adds, why a plain SELECT writes pages, and what 64 open savepoints cost.
Row Versions
Topic 29
Snapshots and Visibility
Three numbers decide what every query sees, and when they are taken decides everything else. The visibility rule stated exactly, the cluster-wide horizon, and what an UPDATE does when it meets a row another transaction just changed.
Visibility
Topic 30
Isolation Levels for Real
Three levels, three prices, and the strawberries fixed three ways: one guarded statement, an explicit row lock, and Serializable with a bounded retry. What 40001 means, whose job it is, and which fix Cartwheel ships.
Isolation Levels
Topic 31
Locks: Rows, Tables, and Deadlocks
The four row lock modes and which pairs conflict, why a foreign-key check blocks a delete but not a price change, and where a row lock is physically stored. Plus SKIP LOCKED as a queue, and what deadlock_timeout really controls.
Locking
Topic 32
Long Transactions: The Cost You Don't See
One open transaction pins the oldest row version the whole cluster may reclaim. The four things that hold the horizon, the one query that names the culprit, and the three timeouts that ship disabled.
Operations