An index can contain every value a PostgreSQL query needs and still require heap access. The missing piece is MVCC visibility: index entries do not carry enough information to prove that a tuple is visible to the current snapshot.

PostgreSQL resolves that gap with the visibility map. An index-only scan checks this compact structure before deciding whether a matching index tuple can be returned without visiting the heap. The result depends on page state, not merely on index coverage.

The visibility map records heap-page state

Each heap relation has a visibility map stored as a separate relation fork. PostgreSQL keeps two bits for each heap page: an all-visible bit and an all-frozen bit.

The all-visible bit states that every tuple on the corresponding heap page is known to be visible to every current and future transaction. The all-frozen bit states that every tuple on the page is frozen, allowing anti-wraparound vacuum work to skip that page until data modification changes its state.

The map is conservative. A set bit is a positive assertion backed by known page state. A clear bit does not prove the opposite condition; it only means PostgreSQL cannot rely on the map to assert it.

That property is central to correctness. An optimization may miss an opportunity when a bit is clear, but it cannot use an optimistic guess to bypass an MVCC check.

Index-only scans still have an MVCC obligation

Consider an index that contains both the predicate column and the selected column:

CREATE INDEX account_email_idx
    ON account (email)
    INCLUDE (display_name);

SELECT display_name
FROM account
WHERE email = 'ada@example.test';

The index has the values needed to evaluate the condition and produce display_name. This makes an index-only scan physically possible for an index access method that supports returning indexed data.

Visibility remains separate. After locating an index tuple, PostgreSQL identifies the heap page referenced by its tuple identifier and checks that page’s all-visible bit.

If the bit is set, the tuple is known to satisfy the visibility requirement without opening the heap page. If the bit is clear, PostgreSQL must visit the heap tuple and perform the normal MVCC visibility check.

As a result, Index Only Scan in an execution plan does not imply zero heap reads. EXPLAIN (ANALYZE) reports Heap Fetches, which exposes cases where the scan had to consult heap tuples despite using the index-only scan node.

Data modification clears the shortcut

Visibility map bits are set by vacuum and cleared when data-modifying operations affect a page. This creates a direct connection between write activity and the amount of heap access an index-only scan can avoid.

A page that has remained stable long enough for vacuum to establish all-visible state can serve matching index-only reads without heap visibility checks. An insert, update, or delete affecting that page clears the relevant visibility information. Subsequent index-only scans may need heap visits until vacuum can establish the page state again.

This behavior also means index design alone cannot guarantee the runtime benefit associated with an index-only scan. A covering index supplies query values, while the visibility map supplies the page-level visibility fact needed to omit heap access. Both conditions matter.

Frequently modified tables can therefore show an Index Only Scan with substantial heap fetches. That is valid execution behavior rather than evidence that the index is malformed.

VACUUM establishes all-visible state

Vacuum maintains the visibility map while processing heap pages. When it can establish that a page contains only tuples visible to all transactions, it can set the all-visible bit.

The same map later reduces vacuum work. A subsequent vacuum can skip pages whose state proves that there is no tuple cleanup to perform, subject to the rules for aggressive vacuuming and freezing.

The two uses reinforce the same page-level model: the map records facts that permit PostgreSQL to omit work safely. For index-only scans, the omitted work is a heap visibility check. For vacuum, it can be a heap-page scan.

This does not make manual vacuuming a universal response to heap fetches. Autovacuum normally maintains table visibility state, and whether pages become all-visible depends on transaction and modification activity. A long-running transaction can also affect when old row versions cease to matter for visibility and cleanup.

All-visible and all-frozen represent different facts

The two visibility-map bits are related but not interchangeable.

All-visible is the bit relevant to index-only heap avoidance. It states that tuple visibility no longer requires per-tuple inspection for the page.

All-frozen goes further. It records that every tuple on the page has been frozen, so transaction ID wraparound maintenance does not need to revisit the page until later modification invalidates that state.

A page can therefore be all-visible without being all-frozen. Normal vacuum can skip some all-visible pages, while an aggressive vacuum may still need to visit pages that have not reached all-frozen state.

This distinction prevents index-only scan behavior from being conflated with transaction ID freezing. The scan needs a safe visibility assertion, not proof that every tuple has already been frozen.

Page granularity shapes the runtime result

The visibility decision is made per heap page. A query touching many index entries can encounter a mixture of all-visible and non-all-visible pages during one scan.

That produces a spectrum rather than a binary outcome. Some matching tuples can be returned directly from index data, while others trigger heap fetches. The execution plan remains an index-only scan because the access method can return the required values from the index; the visibility map determines which individual heap visits can be omitted.

This page granularity is also a useful boundary when interpreting performance changes after write-heavy periods. A small number of modified rows can clear visibility state on the pages containing them, while untouched pages retain their established state.

Covering indexes and the visibility map solve different parts of the same access problem. The index determines whether PostgreSQL has the query data available without reading heap values. The visibility map determines whether PostgreSQL can trust those index entries without consulting heap tuple visibility. Index-only behavior becomes most effective when both conditions remain true at the same time.