Books you read and systems you run, turned into labs. Each exercise makes you predict a specific behavior, run the real thing, and check your prediction against a real transcript — the gap is where the mental model gets built.
Kleppmann & Riccomini, 2nd ed. (2025).
NOT NULL column with a default to a 10M-row Postgres table takes 0.8 ms, not the full rewrite everyone predicts — but a volatile default rewrites it in ~8 s.
WITH RECURSIVE gets it right at any depth. A fixed join count can't express transitive reachability.
@>, not ->> equality.
1/(2×density). Netflix-scale dense is 34 GB.
VACUUM sets the visibility map; then heap fetches drop to zero.
ILIKE '%…%' scans 5M rows (~1.25 s); a tsvector+GIN index does it in ~19 ms, the probe touching 5 buffers — ~65× faster.
int64 round-trips it exactly.
int64 sign-extends to 10 bytes; sint64 zigzag makes it 1. A batch of small negatives is 7.3× larger as int64.
Reading companion — what to read closely, what to skim, where to stop and run one of these, and the passages that get misread.
INSERT … now(), random() gets different values than the leader — why databases ship rows/WAL, not statements.
w + r > n forces the read and write sets to overlap (9/9 combos), yet a sloppy quorum still returns stale.
[eggs] and loses milk; version numbers keep both as siblings, losing nothing.
hash % N; a fixed 1,000-shard layer moves just 9% — 10× less data over the wire, from indirection alone.
hash % N moves 91%, 163k of them pure churn between unchanged nodes.
color=red reads 8 shards locally vs 1 globally; writing one car touches 1 shard locally vs 3 globally. You choose which side pays.
Reading companion — what to read closely, what to skim, where to stop and run one of these, and the passages that get misread.
READ UNCOMMITTED (Postgres accepts it by name) and a concurrent session still reads the old committed value, never the uncommitted one — MVCC makes the dirty read impossible.
value = value + 1 or FOR UPDATE fixes it, and REPEATABLE READ aborts one with 40001.
40001.
FOR UPDATE can't lock a row that doesn't exist yet — SERIALIZABLE (40001) or an exclusion constraint (23P01) closes it.
SELECT … FOR UPDATE and the writer blocks ~1 s.
40P01 and the survivor commits. Consistent lock ordering prevents it.
WHERE matches nothing → UPDATE 0 (a lost update prevented lock-free); at REPEATABLE READ it aborts with 40001 instead.
PREPARE TRANSACTION leaves an in-doubt txn that keeps its row lock and survives a restart in pg_prepared_xacts; only COMMIT/ROLLBACK PREPARED frees it.
SERIALIZABLE, a transaction's second read still returns 0 after another commits 1, and commits with no error — serializable order isn't real-time order.
L(e9)=3 < 4=L(e4) tempts "e9 before e4", but the vector clocks are incomparable — genuinely concurrent. One integer can't tell order from coincidence.
sort barely moves.
UNIQUE constraint makes the retry a no-op — the end-to-end argument.
alice free and both grant it (2 owners); routing claims through one totally-ordered partition grants exactly 1.
Provenance is the PostgreSQL 16 documentation rather than a book. Every transcript below was re-captured on PostgreSQL 16.14 (macOS 26.5.2 / arm64, Docker Engine 29.6.2, image digest pinned) — see the backlog, which records what re-verification changed.
READ COMMITTED → REPEATABLE READ → SERIALIZABLE in two psql sessions; each level produces the anomaly the next one prevents.
pg_stat_activity, pg_blocking_pids() and pg_locks. A blocked query is not a black box.
UPDATE is an INSERT plus a tombstone, and the idle transaction everyone blames for blocking VACUUM turns out not to.
rows= and actual rows= disagree by a factor of a million, you get a Nested Loop where a Hash Join belonged — same SQL, same schema, same data.
COMMIT returns your rows are not in the table file. SIGKILL the server and watch the table get reconstructed from the log.
1, then watch the byte distance between the two nodes climb and drain.
Provenance is the Docker docs and the Linux namespaces(7) / cgroup-v2 kernel documentation. Every transcript re-captured on Docker Engine 29.6.2 (Linux VM kernel 6.12.76-linuxkit), images pinned — see the backlog.
docker run is a clone() with extra flags.
--memory=50m writes a number into a file. Exceed it and the kernel doesn't return NULL from malloc or raise anything — it sends SIGKILL, and your process gets no turn.
127.0.0.11 answers NXDOMAIN for a container one bridge away, and DNS and reachability fail independently.
Source: github.com/atharh/study · each exercise's .md is the source of truth; the .html is its rendered companion.