Field Guide / Reference / Storage
Cheat sheet

Slotted page layout, on one page

The within-page layout that turns a fixed-size block of bytes into addressable, variable-length rows: header, line-pointer array growing down, tuple data growing up, free space closing in the middle.

page header slot count, pd_lower, pd_upper, lsn, flags slot0 slot1 slot2 ... free space pd_upper minus pd_lower bytes tuple0 tuple1 tuple2 pd_lower array grows down pd_upper data grows up 0 N-1
Figure 1. The two ends close on a shrinking free space. The slot array (offsets 0 upward) grows down as pd_lower rises; tuple data grows up as pd_upper falls. The page is full when they meet. Each slot indirects to a tuple offset (dashed arrow), so a tuple can move during compaction while its record id stays fixed.
Keep this one thing

A record id is (page id, slot number), never a raw byte offset. The slot indirects to the in-page offset, so the engine can compact and move tuples while the id an index holds never changes.

Slotted page header (generic)

The minimum a slotted page header must record to drive insert, lookup, and the full-page check.
FieldPurpose
slot countnumber of slots in the line-pointer array; index 0 to count-1.
free-space pointer(s)boundary between used and free space; in PostgreSQL the pair pd_lower / pd_upper.
free-space startoffset where the array ends and free space begins (data side grows from the page end inward).
page lsnWAL position of the last change; page is not flushed until WAL up to this lsn is durable.
flags / checksumpage state bits and an optional integrity checksum.
special space ptrstart of access-method-private trailer; empty for an ordinary heap page.

PostgreSQL PageHeaderData (24 B)

Header at offset 0 of every 8 KB heap page.
FieldSizeMeaning
pd_lsn8 BWAL lsn of last change (write-ahead rule).
pd_checksum2 Bpage checksum.
pd_flags2 Bpage state flags.
pd_lower2 Boffset to start of free space (end of ItemId array). Grows up.
pd_upper2 Boffset to end of free space (start of tuples). Grows down.
pd_special2 Boffset to special space.
pd_pagesize_version2 Bpage size and layout version.
pd_prune_xid4 Boldest unpruned xmax, or 0.

ItemId / line pointer (4 B)

ItemIdData bitfields, storage/itemid.h. One per slot.
FieldBitsMeaning
lp_off15offset to tuple from page start. 2^15 caps a page at 32 KB.
lp_flags2line-pointer state (below).
lp_len15byte length of the tuple.
lp_flags states.
ValueStateNote
0LP_UNUSEDfree slot, lp_len=0.
1LP_NORMALlive, lp_len>0.
2LP_REDIRECTHOT redirect to newer version on same page.
3LP_DEADdead, reclaimable by VACUUM.

PostgreSQL HeapTupleHeaderData

Per-row header, access/htup_details.h. Documented minimum 23 B before user data; t_hoff rounds it to MAXALIGN.
FieldSizeMeaning
t_xmin4 Binserting transaction id.
t_xmax4 Bdeleting transaction id; 0 if live. The tombstone marker.
t_cid / t_xvac4 Bcommand id or vacuum xid (overlaid).
t_ctid6 Btid of this or a newer version; the update-chain pointer.
t_infomask22 Battribute count plus flag bits.
t_infomask2 Bflag bits, including HEAP_HASNULL.
t_hoff1 Boffset to user data; multiple of MAXALIGN (typically 8).
null bitmapvarpresent only if HEAP_HASNULL; 1 bit per column.

Variable-length records and the NULL bitmap

Why these two mechanisms exist

Fixed-length attributes sit at a computable offset. A varchar/text/bytea breaks that: you cannot find column N+1 without knowing the length of column N. So variable-length data carries a length prefix (or an in-tuple offset array). NULL gets a bitmap, not a sentinel, because any sentinel byte could be a legal value.

ConcernEncoding
Fixed-length columnraw bytes at a precomputed offset; padded to its natural alignment.
Variable-length columnlength prefix then bytes; reader advances by the stored length to reach the next column.
NULL bitmapafter the fixed header, only if HEAP_HASNULL. One bit per column: 1 = present, 0 = null. A null column stores zero data bytes.
Alignment (MAXALIGN)fields padded to natural boundaries; user data starts at t_hoff. Column order changes row size because of pad gaps (put wide types before narrow ones).
Oversized value (TOAST)a tuple cannot span pages; values past ~2 KB are compressed and/or moved out of line, leaving an 18 B pointer in the tuple. Up to 1 GB per field.
Exam traps

The slot array and tuple data grow in opposite directions. A NULL is not a zero or sentinel in the column bytes; it is a 0 bit in the header bitmap and occupies no data area. A tid is not an arithmetic disk address; it is (page, slot) and the slot can move on compaction.

Quick rules: insert, delete, update within a page

What each operation does to the slot array, the data area, and the free-space pointers.
OpSteps and invariant
Insert Check fit: free space = pd_upper - pd_lower must hold tuple_len + slot_size; else need a new page (see the FSM). Write tuple at pd_upper - tuple_len (data grows down toward the array); lower pd_upper. Allocate a slot at the front, set lp_off/lp_len, raise pd_lower by 4 B. O(1). A free LP_UNUSED slot may be reused instead of extending the array.
Delete Do not physically erase under MVCC. Stamp t_xmax with the deleting xid (tombstone). The line pointer becomes LP_DEAD on pruning; VACUUM later reclaims the slot and data. Old readers still see the row until the horizon passes. Cheap and non-blocking, at the cost of bloat.
Update Insert the new version, set the old tuple's t_ctid to point at it (update chain). If the new version fits on the same page and no indexed column changed, use HOT: keep an LP_REDIRECT line pointer so indexes need not be touched. Otherwise the new version may land on another page and indexes get a new entry.
Compact Slide live tuples up to close gaps left by deletes, rewriting each slot's lp_off. Record ids stay stable because indexes hold the slot number, not the offset. Raises usable free space; done lazily, not on every delete.
In real systems

PostgreSQL fixes the page at 8 KB and uses the pd_lower / pd_upper pair as its free-space pointers [PG: Database Page Layout]. SQLite uses the same slotted idea inside each B-tree page (cell pointer array growing down, cell content growing up) and reclaims intra-page gaps with chained freeblocks rather than a VACUUM process [SQLite file format].