PostgreSQL keeps tuple visibility metadata in heap rows, but checking every heap tuple is unnecessary when an entire page is already known to satisfy stronger conditions. A visibility map stores that page-level state in a compact relation fork.
Each heap page has two corresponding bits. The all-visible bit records that every tuple on the page is visible to every current and future transaction. The all-frozen bit records that every tuple on the page is frozen. Those facts let PostgreSQL avoid work in index-only scans and vacuum operations without moving MVCC visibility data into indexes.
The map is deliberately conservative. A set bit asserts a condition PostgreSQL has established. A clear bit does not assert the opposite; it can simply mean that the page has not yet been proven to satisfy the condition.
Two bits describe distinct page properties
The all-visible and all-frozen states are related, but they serve different decisions.
An all-visible page contains no tuple that requires a transaction-specific visibility check. This is the state an index-only scan can use to avoid fetching the matching heap tuple. PostgreSQL indexes generally identify heap tuples but do not carry enough MVCC information to decide tuple visibility on their own.
An all-frozen page goes further. Its tuples no longer require future freezing work for their transaction identifiers. Vacuum can use that state when deciding which pages can be skipped, including during maintenance concerned with transaction ID age.
The distinction matters because a page can be all-visible without yet being all-frozen. Visibility to current and future transactions does not itself mean every relevant transaction identifier on the page has already been frozen.
Index-only scans consult page state
An index can contain every column required by a query and still require heap access. PostgreSQL must also establish that each candidate tuple is visible to the current transaction.
For an index-only scan, the executor can consult the visibility map using the heap block referenced by an index entry. If that heap page is marked all-visible, the executor can return values from the index without visiting the heap for the visibility check.
index entry
|
+-- heap block 418
|
+-- VM all-visible = 1 -> heap fetch can be skipped
|
+-- VM all-visible = 0 -> inspect heap tupleThis makes index coverage and page visibility separate conditions. Adding included columns can make a query structurally eligible for an index-only scan, while frequent writes can still cause heap fetches because modified pages lose their all-visible state.
The visibility map is much smaller than the heap because it stores only two bits per heap page. Consulting it can therefore avoid a heap access with little metadata overhead.
Vacuum establishes reusable page facts
VACUUM is responsible for setting visibility map bits after examining heap pages and establishing the required conditions. Data-modifying operations clear relevant state when a page can no longer be assumed to satisfy it.
That asymmetry protects correctness. Clearing a bit early only causes extra work later. Setting a bit without establishing its condition could allow PostgreSQL to skip work that is required for visibility or maintenance.
A page that has settled after a period of writes can become all-visible after vacuum processes it. If its tuples are also frozen, the all-frozen bit can be set as well. A later modification invalidates the page-level shortcut until maintenance can establish the state again.
This behavior connects write patterns to read behavior. A mostly static table can accumulate a large all-visible portion, giving index-only scans many opportunities to avoid heap access. A table whose pages are continually modified may expose fewer such opportunities even when suitable indexes exist.
Vacuum also uses the map to limit scanning
The map is not only an executor optimization. Vacuum consults it to decide which heap pages require inspection.
Pages known to contain no tuples needing cleanup can be skipped during ordinary vacuum processing. All-frozen pages provide an even stronger condition: maintenance concerned with old transaction identifiers can skip them because there is no remaining freezing work on those pages.
Not every vacuum pass must therefore read every heap page. The visibility map preserves facts established by earlier work and lets later maintenance focus on pages whose state still needs examination.
Aggressive vacuum behavior differs from an ordinary pass because transaction ID aging can require inspection of pages that are all-visible but not all-frozen. The separate bits allow PostgreSQL to distinguish those cases rather than treating visibility and freezing as one property.
A clear bit is not evidence of a bad page
The conservative design is useful when interpreting visibility data operationally. An unset all-visible bit does not prove that the page contains a tuple invisible to some transaction. It only means PostgreSQL cannot currently rely on the stronger all-visible assertion.
The same principle applies to all-frozen state. Absence of the bit means the page cannot be skipped on the basis of that assertion alone.
The pg_visibility extension exposes visibility map state for inspection. It can report all-visible and all-frozen information by block and summarize those counts for a relation. That can help separate index design from heap visibility when an index-only execution plan still reports heap fetches.
Catalog estimates provide a broader view. pg_class.relallvisible records an estimate of pages marked all-visible, while relallfrozen records an estimate of pages marked all-frozen. Exact block-level inspection and catalog estimates serve different diagnostic needs.
Page state links MVCC, indexes, and maintenance
The visibility map is small, but it sits at a boundary shared by several PostgreSQL subsystems. MVCC determines whether tuple versions are visible, vacuum establishes page-level facts, and index-only scans consume one of those facts to avoid heap reads.
That separation keeps indexes from carrying full tuple visibility state while still allowing stable heap pages to become cheaper to query. It also lets vacuum retain knowledge from earlier passes instead of rediscovering every page condition on each run.
When index-only scans perform more heap fetches than expected, index contents are only part of the picture. The visibility state of the referenced heap pages is another condition, and it changes with writes and subsequent vacuum activity.