Lesson 9 · Internals & data structures
VACUUM & bloat
The clean-up MVCC can't live without — and the classic "why is my table huge?"
Your win: explain why MVCC requires VACUUM, what VACUUM does, and why autovacuum matters for a write-heavy table like yours.
MVCC's leftovers
Every UPDATE/DELETE leaves a dead tuple
— an old version no transaction can see any more (Lesson 6). Left alone, dead tuples
accumulate: the table's files grow, pages fill with corpses, and scans read more to find
live rows. That's bloat.1
What VACUUM does
VACUUM; VACUUM FULL rewrites the table to return space to the OS
but takes an exclusive lock). (2) Update the visibility map (which pages
are all-visible → enables index-only scans, Part 3). (3) Refresh planner
statistics (ANALYZE, Part 4). (4) Freeze
old transaction ids to prevent txid wraparound.
Autovacuum runs VACUUM automatically once a table's dead-tuple count crosses a threshold — so you usually don't run it by hand, but you must not disable it on a churning table.
A related optimisation: HOT updates
If an UPDATE changes no indexed column and the new tuple fits on the
same page, Postgres does a Heap-Only Tuple update: it chains the new
version on the page and skips touching the indexes — cheaper, and it lets space be reused
without a full VACUUM.2
UPDATE … SET deleted_at = now()
and every ON CONFLICT DO UPDATE upsert creates a dead tuple. Without
autovacuum, emails / conversation would bloat and slow down.
Bonus: a soft-delete that touches only deleted_at (not an indexed column) is
a prime HOT candidate — index bloat avoided.
DELETE + VACUUM makes the space reusable, it doesn't
shrink the file. Only VACUUM FULL (or pg_repack) returns space
to the OS, at the cost of a heavy lock.
PostgreSQL — Routine Vacuuming + HOT updates
The operational chapter on VACUUM/autovacuum/freezing, and the storage note on Heap-Only Tuples.
→ postgresql.org/docs — Routine Vacuuming
→ The Internals of PostgreSQL — HOT
Check yourself (from memory)
Q1. VACUUM exists to…
Q2. Dead tuples are created by…
Q3. Autovacuum is triggered by…
VACUUM FULL rewrites to return space to the OS
but locks the table), updates the visibility map (enabling index-only scans), refreshes
planner statistics (ANALYZE), and freezes old transaction ids to prevent wraparound.
AUTOVACUUM runs it once dead tuples cross a threshold — essential for our soft-delete +
upsert churn.