Skip to content
In this guide

Guide · The plan, node by node

Index Only Scan

An Index Only Scan answers the query from the index and never opens the table. It is possible when every column the query needs is in the index, as a key or in its INCLUDE list. invoices_customer_created_idx is (customer_id, created_at DESC) INCLUDE (status), so SELECT created_at, status FROM billing.invoices WHERE customer_id = $1 can be answered by it without touching the heap.

Why it matters

The catch is visibility. An index does not know which transactions can see a row, so PostgreSQL checks the visibility map. That is one bit per page, set when every row on it is visible to everyone. Pages with the bit are skipped. Pages without it force a heap fetch, the read the scan was meant to avoid. EXPLAIN ANALYZE prints Heap Fetches: N. Zero is the goal. A count near the rows returned means you are paying for an Index Scan. invoices has had 1,000,000 updates, and each one clears the bit on its page until the next VACUUM sets it again.

What to do

  1. VACUUM billing.invoices. Only vacuum sets visibility map bits. ANALYZE does not.
  2. If the fetches come back, the table changes faster than autovacuum keeps up. Lower its threshold:
ALTER TABLE billing.invoices SET (autovacuum_vacuum_scale_factor = 0.02);
  1. An Index Scan where you expected an Index Only Scan means a column is missing from the index. INCLUDE adds it without making it part of the key.

In the Plan tab

The wheel matches the Index Only Scan pattern, the fourth of the five the TABLE WHEEL guide draws.

The five ways a scan reaches a table, as the TABLE WHEEL guide draws them.

The five ways a scan reaches a table, as the TABLE WHEEL guide draws them.

  1. Seq Scan reads the whole rim: every page, whatever the query needed.
  2. Index Scan · in order fetches every row from the table through the spokes, in a short arc. Each spoke is a page read the Index Only Scan avoids.
  3. Index Scan · scattered fetches the same way, across the whole rim. The cost the Index Only Scan saves is largest here.
  4. Index Only Scan is this node: the hub answers the query and the rim stays dark. The lit segments are heap fetches, pages the visibility map could not vouch for, and each one is the read the scan was meant to avoid.
  5. Bitmap Heap Scan collects the pages first and reads each once. It still reads the heap.

In the guide for a real wheel the fetches are labelled Lit · heap fetches. The count itself is not in the inspector. Hover the wheel's spokes for an Index lookups card with Heap fetches and the number, or the rim for Heap visited as a share of the table. The raw figure is in Raw plan JSON as Heap Fetches.

Caveats

  • An Index Only Scan can carry a Filter on a column the index lacks. The heap is fetched for it anyway, and the node is an Index Scan in all but name.

Last checked against Seqora 0.3.2, PostgreSQL 17 on .

Found a bug, missing a feature, or just curious? Write to me. Every email lands in my own inbox, not a ticket queue.