Inside the 8KB Page: PostgreSQL Page Layout Visualized
Interactive visualization of PostgreSQL's 8KB page: header, line pointers, free space, and tuple data.
Each 8KB page has four key regions: a 24-byte header, line pointers (4 bytes each, growing downward), free space, and tuple data (packed upward from the bottom).
Click Insert Row to watch the opposing growth directions in action. The red × on any tuple marks it dead (sets t_xmax) without reclaiming space, the same as PostgreSQL's MVCC, leaving holes between the live tuples. Hit VACUUM to remove the dead tuples, compact the survivors into one contiguous block, and free their line pointers for reuse. A new Insert Row can then land in the reclaimed space. Click any region or tuple card for byte-level details.
Rows
0
pd_lower
24
pd_upper
8192
Free Space
8168 B
Page Inspectorclick a region on the mapsmall regions enlarged for clarity · thin bar shows actual proportions
Header24 B
Free Space8168 B
08192
sql>
Tuple Inspectorpage_demo (id int, title text, created_at timestamptz, amount numeric)
insert rows to watch the page fill up
What VACUUM does to the page
VACUUM walks the page, drops the dead tuples, and runs PageRepairFragmentation to slide the surviving tuples together so the freed bytes coalesce into one contiguous block of free space. The live tuples move, but they keep their line pointer slot numbers: an index entry points at a tuple by (block, offset), so the slot number has to stay stable even as the tuple's physical offset changes.
A dead tuple's line pointer does not jump straight to free. After the DELETE it is still LP_NORMAL; pruning marks it LP_DEAD; only once VACUUM has cleared the matching index entries does it become LP_UNUSED and available again. The next INSERT fills an LP_UNUSED slot before extending the array, so a low slot number can carry a brand-new row. Trailing unused slots are dropped from the end of the array, pulling pd_lower back down; interior unused slots stay put as gaps waiting to be reused.