Pages on disk
PostgreSQL stores the books table as one file on disk, cut into pages of 8,192 bytes (8 kB). A page is the unit of work: the database reads and writes whole pages, never a single row.
To touch a row, PostgreSQL first copies its page from disk into shared buffers, a pool of page slots in memory. The default pool is 128 MB: room for 16,384 pages. Later reads of page 0 can skip the disk while it stays there.
Inside a page
Every page starts with a 24-byte header. Two of its numbers mark the free space: pd_lower, where it starts (24), and pd_upper, where it ends (8,192). An empty page has 8,168 bytes free.
CREATE TABLE gives books three columns. INSERT puts Dune at the very end of the page: 40 bytes at offset 8,152. A 4-byte line pointer, number 1, goes after the header. It says where the row is and how long.
Coraline and Holes follow. Line pointers grow down from the header, rows grow up from the end, and the free space in between shrinks to 8,028 bytes. Coraline takes 48 bytes: rows start on 8-byte boundaries.
A row's address is its ctid: (page, line pointer). Holes is (0,3): page 0, pointer 3, which says offset 8,064, length 40. The pointer holds the offset, so a row can move inside its page and keep its ctid; indexes store ctids.
Changing a page
UPDATE does not overwrite Holes. It writes a new version at (0,4), offset 8,024, and marks the old one as replaced. The page in memory is now dirty. Older transactions may still read the old version; VACUUM frees it later.
The changed (dirty) page stays in memory. Its change is first written to the write-ahead log (WAL) on disk. Only after that may the page itself be written back to the file, later, at a checkpoint.
Each row like Holes costs 40 bytes plus a 4-byte pointer, so about 185 fit in one page. When pd_lower meets pd_upper, new rows go to another page with room, such as page 1.
What happens
;; step 1/9 · pages table books -> one file on disk the file is a row of pages, 8192 bytes each page 0 bytes 0 .. 8191 page 1 bytes 8192 .. 16383 page 2 bytes 16384 .. 24575 read or write one row? move its whole page
The page in the figure is not to scale: the header and these 3 rows use only 164 of its 8,192 bytes. Row sizes assume a usual 64-bit build, where rows start on 8-byte boundaries, and short text values (under 127 bytes) with a 1-byte length header. Long values are moved out of the page (TOAST).