How it works
The explanation, with examples you can run.
Watching a row get a new version
Every row version carries hidden system columns: xmin, the transaction that created it; xmax, the transaction that deleted or replaced it (0 if none); and ctid, its physical position (page, item).
SELECT ctid, xmin, xmax, * FROM accounts WHERE id = 1;-- ctid | xmin | xmax | id | owner | balance_cents-- (0,1) | 875 | 0 | 1 | ana | 10000 UPDATE accounts SET balance_cents = balance_cents - 100 WHERE id = 1; SELECT ctid, xmin, xmax, * FROM accounts WHERE id = 1;-- (0,3) | 876 | 0 | 1 | ana | 9900The row moved from item 1 to item 3 of page 0 and was created by transaction 876. The old version at (0,1) is still in the page, with xmax = 876. Any snapshot taken before 876 committed still sees 10000. Once no snapshot can see the old version, it is a dead tuple:
SELECT n_live_tup, n_dead_tup FROM pg_stat_user_tables WHERE relname = 'accounts';-- n_live_tup | n_dead_tup-- 2 | 1VACUUM
VACUUM scans the table, removes dead versions from the table and its indexes, and records the freed space in the free space map so new rows can reuse it. It runs alongside normal reads and writes. It usually does not give space back to the operating system (only empty pages at the very end of the file). VACUUM FULL rewrites the whole table compactly, but holds an ACCESS EXCLUSIVE lock while it does, which blocks even reads.
VACUUM (VERBOSE) accounts;INFO: finished vacuuming "shop.public.accounts": index scans: 0tuples: 1 removed, 2 remain, 0 are dead but not yet removableVACUUM also sets bits in the visibility map (which index-only scans rely on) and freezes old rows (below).
Autovacuum
The autovacuum launcher runs VACUUM and ANALYZE on tables that have changed enough. With the defaults, a table is vacuumed when its dead tuples exceed 50 plus 20% of its rows (autovacuum_vacuum_threshold, autovacuum_vacuum_scale_factor). On a 100-million-row table, that is 20 million dead rows before it starts, so large, busy tables often get a lower per-table scale factor:
ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.02);What stops VACUUM
VACUUM can only remove versions that are older than every snapshot still open. A transaction left open for hours (often a session idle in transaction, or a long report) pins them all. VACUUM VERBOSE reports them as "dead but not yet removable". Find the culprit in pg_stat_activity by xact_start, and set idle_in_transaction_session_timeout to end abandoned sessions.
Wraparound
Transaction ids are 32-bit counters, so they wrap around after about four billion. To keep old rows visible, VACUUM freezes them, marking them as visible to everyone regardless of id. If freezing falls too far behind, PostgreSQL warns and, as a last resort, stops assigning new transaction ids until a VACUUM catches up. Healthy autovacuum prevents this; monitor age(datfrozenxid) in pg_database to be sure.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.