Row Store Layout

How a Row Store Lays Out Data on Disk

PostgreSQL 1,289 stores a table as a heap of 8 kB pages: a 24-byte header, then 4-byte line pointers growing forward, while tuples fill from the end backward. The bundled pageinspect extension shows it for mart.sales:

Inside the first heap page of mart.sales
CREATE EXTENSION IF NOT EXISTS pageinspect;
SELECT lower, upper FROM page_header(get_raw_page('mart.sales', 0));
SELECT lp, lp_off, lp_len, t_hoff FROM heap_page_items(get_raw_page('mart.sales', 0)) LIMIT 3;
SELECT count(*) AS rows_on_page, pg_relation_size('mart.sales') / 8192 AS pages
FROM heap_page_items(get_raw_page('mart.sales', 0));
Output
 lower | upper
-------+-------
   256 |   328
 lp | lp_off | lp_len | t_hoff
----+--------+--------+--------
  1 |   8056 |    136 |     32
  2 |   7928 |    126 |     32
  3 |   7784 |    144 |     32
 rows_on_page | pages
--------------+-------
           58 |  2432

Line pointers end at byte 256 (24 + 58 x 4) and the lowest tuple starts at 328: the page is full. Each tuple spends 32 bytes (t_hoff) on visibility data, a null bitmap and padding, then stores all 15 columns side by side. Fetching one order line is one page read; summing gross_amount reads all 2,432 pages (19.9 MB) and deforms every tuple, walking its columns in order to reach the wanted one.