PostgreSQL VACUUM at the Page Level Visualized

Interactive visualization of VACUUM across three heap pages. Run DELETE and watch nothing get reclaimed, page-prune to reach LP_DEAD, then step VACUUM through heap scan, index cleanup, and heap cleanup that frees the slots.

A DELETE in PostgreSQL reclaims nothing on its own. It stamps the tuple's t_xmax and moves on: the row is still physically on the page, the indexes still point at it, and the free space map still thinks the page is full. Nothing is reclaimed until VACUUM arrives.

Run DELETE and watch the dead tuples pile up with no change to free space. Hit Page-prune to fire opportunistic cleanup: the bytes come back and the line pointers go LP_DEAD, but the slots stay pinned because the index still points at them, and pruning can't reach the index or the other pages. Then step VACUUM through its three phases: heap scan and prune, index cleanup (the part pruning couldn't touch), and heap cleanup that flips LP_DEAD to LP_UNUSED. Finish with INSERT: rows reuse the reclaimed space instead of extending the table. Click any line pointer for its full state.

Live Tuples
18
Dead Tuples
0
LP_UNUSED
0
Index Entries
18
FSM Free
960 B
Table Size
24 KB
sql>
-- INSERT 18 rows across 3 heap pages
1 Heap scan + prune reclaim bytes → LP_DEAD
2 Index cleanup the part pruning can't reach
3 Heap cleanup LP_DEAD → LP_UNUSED, VM
vacuum_demo_pkey (btree on id) 18 entries
Make dead tuples
Reclaim
Reset
Line pointer state
LP_NORMAL: live tuple
LP_NORMAL: dead (t_xmax set), still on the page
LP_DEAD: known dead, index entries not yet removed
LP_UNUSED: reclaimed, slot reusable
Why pruning isn't enough
Opportunistic page pruning works inside one page: it reclaims the dead tuples' bytes (the FSM updates) and flags their line pointers LP_DEAD. But it can't remove the index entries that still point at those TIDs, so it can't free the slots, and it can't reach across pages. That's where it stops. Only VACUUM scans every index (phase 2), then finishes the heap (phase 3): LP_DEADLP_UNUSED, freeing the slots the index used to pin.
Click any line pointer to inspect it.
Line Pointer Inspector click a line pointer to inspect

Why page pruning isn't enough

Opportunistic page pruning is the shortcut: it runs during ordinary reads and writes, works entirely inside a single page, and does real work. It strips the storage off dead tuples (the bytes come back and the free space map updates) and flags their line pointers LP_DEAD. But that is where it stops. It cannot remove the index entries that still reference those TIDs, so it cannot free the LP_DEAD slots, and it cannot reach across pages.

VACUUM is what finishes the job. Phase 1 scans and prunes the heap, reclaiming the dead tuples' bytes, setting their line pointers LP_DEAD, and collecting the TIDs. Phase 2 walks every index and removes the entries pointing at them, the step pruning can never do. Only then is phase 3 safe: those LP_DEAD line pointers become LP_UNUSED and the slots are reusable. The distinction that trips people up is that the bytes were already reclaimed during the prune; what VACUUM adds is cleaning the index and releasing the slots, everywhere, not just on a page someone happened to read.