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 ANALYZEshowsHeap 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 (seeautovacuum_vacuum_insert_thresholdfor insert-heavy tables). - VACUUM skips all-visible pages. An aggressive VACUUM (when the table's
relfrozenxidage exceedsvacuum_freeze_table_age, withVACUUM 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 | 0Both 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
storage/freespace/README(the design),freespace.c(the API and the tree of FSM pages),fsmpage.c(the binary tree inside one page) andindexfsm.c(the "page is free or not" variant used by indexes).visibilitymap.c; its header comment is the VM's design note, withvisibilitymapdefs.hfor the bit values.- Main users:
hio.c(inserts),vacuumlazy.c(VACUUM) andnodeIndexonlyscan.c.
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 triesRelationGetTargetBlock()(the block this backend used last), thenGetPageWithFreeSpace()→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 toRelationExtensionLockWaiterCount(), and records the spare ones withRecordPageWithFreeSpace(). - 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_slotis even updated under a share lock, and redo of heap records adjusts entries viaXLogRecordPageWithFreeSpace().
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 recheckPD_ALL_VISIBLEafterwards), so no VM read happens while the content lock is held. visibilitymap_set()(VACUUM, and COPY FREEZE viaheap_multi_insert()) is WAL-logged throughlog_heap_visible()(XLOG_HEAP2_VISIBLE) and goes together withPD_ALL_VISIBLEon the heap page.- Clearing is not logged separately:
heap_insert(),heap_update()andheap_delete()clear the bits andPageClearAllVisible()inside their critical section, and their WAL record carries a flag so redo clears them too.heap_lock_tuple()clears onlyVISIBILITYMAP_ALL_FROZEN. - Readers use
VM_ALL_VISIBLE()/VM_ALL_FROZEN().IndexOnlyNext()skips the heap fetch when the bit is set;visibilitymap_count()feedsrelallvisible/relallfrozen. - VACUUM's skipping lives in
heap_vac_scan_next_block()andfind_next_unskippable_block()(SKIP_PAGES_THRESHOLD= 32); 18's eager freezing is set up inheap_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