DB Internals AtlasPostgreSQL 18.6 Home About

Storage

Free space map and visibility map

Each table has two small companion files. The free space map tells INSERT and UPDATE which page has room; the visibility map tells VACUUM and index-only scans which pages need no attention.

In plain words

Imagine a huge car park. Driving through every row to find a free spot would be slow, so there is a sign at the entrance showing which levels still have space. The free space map (FSM) is that sign for a table: for every page it remembers roughly how many bytes are free, organised as a small tree so PostgreSQL can find a page with enough room in a few steps.

The visibility map (VM) is a checklist with two ticks per page. All-visible means every row on the page is old enough that every transaction can see it. All-frozen means the rows are also permanently marked as old, so they never need freezing again.

Those ticks save a lot of work. VACUUM skips pages that are already all-visible, and an index-only scan can answer from the index alone without visiting the table, as long as the page is ticked. Any change to a page removes its tick until the next VACUUM.

For DBAs and operators

Free space map (<relfilenode>_fsm)

One byte per heap page stores a category: free space divided by 32 (so 0–255). FSM pages are organised as a tree of binary max-trees: each upper node holds the maximum of its children, so the root tells you the largest free space anywhere. A search descends towards any leaf with enough space; each FSM page remembers a "next slot" (fp_next_slot) where the next search starts, so concurrent inserters spread across pages rather than piling onto one.

  • The FSM is only a hint. It is updated mainly by VACUUM and when an inserter finds a page fuller than advertised. If no page qualifies, the table is extended (when several backends are waiting to extend it, several pages are added at once).
  • Tables without VACUUM history (e.g. after a bulk load) may have no FSM yet.
  • B-tree indexes have an FSM too, but it only tracks completely empty, recyclable pages.
CREATE EXTENSION pg_freespacemap;
SELECT blkno, avail FROM pg_freespace('orders') WHERE avail > 1000 LIMIT 10;

Visibility map (<relfilenode>_vm)

Two bits per heap page: ALL_VISIBLE and ALL_FROZEN. One 8 kB VM page covers about 32,000 heap pages (256 MB of table). Only VACUUM sets the bits (and COPY FREEZE); any INSERT, UPDATE or DELETE on the page clears them (and the PD_ALL_VISIBLE flag in the page header); a row lock (SELECT … FOR UPDATE) clears only the all-frozen bit.

  • Index-only scans check the VM bit for each heap page. If it is set, the heap is not visited. EXPLAIN ANALYZE shows Heap Fetches: N; a high number means many referenced heap pages are not currently marked all-visible. Investigate the write pattern and vacuum cadence before changing settings (see autovacuum_vacuum_insert_threshold for insert-heavy tables).
  • VACUUM skips all-visible pages. An aggressive VACUUM (when the table's relfrozenxid age exceeds vacuum_freeze_table_age, with VACUUM FREEZE, and every anti-wraparound autovacuum) only skips all-frozen pages. Pages are only skipped in runs of at least 32 consecutive skippable pages, so that OS read-ahead keeps working.
  • Since PostgreSQL 18, normal VACUUM also eagerly scans some all-visible but not all-frozen pages to freeze them early, capped by vacuum_max_eager_freeze_failure_rate (default 0.03). The VACUUM log shows … scanned, N eagerly scanned.
SELECT relpages, relallvisible, relallfrozen   -- relallfrozen is new in 18
  FROM pg_class WHERE relname = 'orders';

CREATE EXTENSION pg_visibility;
SELECT * FROM pg_visibility_map_summary('orders');
-- all_visible | all_frozen
--         443 |          0

Both files are tiny. FSM updates are not WAL-logged (it is only a hint and corrects itself over time); VM changes are WAL-logged, so the VM is crash-safe. A corrupted VM can make index-only scans return wrong results: pg_check_visible() and pg_truncate_visibility_map() in pg_visibility exist for that case.

For developers: inside the source

Where it lives

FSM internals

Each FSM page is an FSMPageData: a binary max-tree in fp_nodes[] and the search hint fp_next_slot. Pages form a tree too, FSM_TREE_DEPTH (3 with 8 kB blocks) levels deep, with the root at block 0 of the _fsm fork. Values are categories from fsm_space_avail_to_cat() (free bytes / 32).

  • RelationGetBufferForTuple() first tries RelationGetTargetBlock() (the block this backend used last), then GetPageWithFreeSpace() → fsm_search() → fsm_search_avail(). If the page turns out too full, RecordAndGetPageWithFreeSpace() corrects the entry and asks again.
  • If nothing fits, RelationAddBlocks() extends the relation, adding extra pages in proportion to RelationExtensionLockWaiterCount(), and records the spare ones with RecordPageWithFreeSpace().
  • VACUUM records free space per page and calls FreeSpaceMapVacuumRange() to fix the upper levels.
  • Not WAL-logged: FSM pages are only protected by buffer content locks, fp_next_slot is even updated under a share lock, and redo of heap records adjusts entries via XLogRecordPageWithFreeSpace().

VM internals

VISIBILITYMAP_ALL_VISIBLE (0x01) and VISIBILITYMAP_ALL_FROZEN (0x02), two bits per heap block, HEAPBLOCKS_PER_PAGE blocks per VM page.

  • Writers that may have to clear a bit pin the VM page with visibilitymap_pin() before locking the heap page (and recheck PD_ALL_VISIBLE afterwards), so no VM read happens while the content lock is held.
  • visibilitymap_set() (VACUUM, and COPY FREEZE via heap_multi_insert()) is WAL-logged through log_heap_visible() (XLOG_HEAP2_VISIBLE) and goes together with PD_ALL_VISIBLE on the heap page.
  • Clearing is not logged separately: heap_insert(), heap_update() and heap_delete() clear the bits and PageClearAllVisible() inside their critical section, and their WAL record carries a flag so redo clears them too. heap_lock_tuple() clears only VISIBILITYMAP_ALL_FROZEN.
  • Readers use VM_ALL_VISIBLE() / VM_ALL_FROZEN(). IndexOnlyNext() skips the heap fetch when the bit is set; visibilitymap_count() feeds relallvisible/relallfrozen.
  • VACUUM's skipping lives in heap_vac_scan_next_block() and find_next_unskippable_block() (SKIP_PAGES_THRESHOLD = 32); 18's eager freezing is set up in heap_vacuum_eager_scan_setup().

Observe and experiment

pg_freespacemap (pg_freespace()), pg_visibility (pg_visibility_map(), pg_check_visible(), pg_check_frozen(), pg_truncate_visibility_map()) and pageinspect's fsm_page_contents() to dump the tree inside one FSM page. Their regression tests are in contrib/pg_visibility/sql and contrib/pg_freespacemap/sql.

Parameters that shape this

vacuum_freeze_min_age vacuum_freeze_table_age vacuum_max_eager_freeze_failure_rate autovacuum_vacuum_insert_threshold enable_indexonlyscan

Related topics

Page layout VACUUM Autovacuum: when tables get vacuumed Scan methods XID wraparound and freezing HOT updates