Index Scan
Appears in EXPLAIN asIndex Scan
Walks an index to find matching rows, then fetches each from the table.
What it is
An Index Scan descends a B-tree (or other index) to locate matching entries, and for each match fetches the corresponding row from the table heap. Rows come back in index order.
When the planner picks it
When an index covers the query's conditions and the planner expects relatively few matching rows, or when it needs the rows in the index's sort order (e.g. for an ORDER BY).
Is it good or bad?
Usually excellent for selective lookups. It can become slow if it matches many rows, because each match costs a separate (often random) heap fetch — past a few percent of the table, a Bitmap Heap Scan or Seq Scan is cheaper.
In depth
How it works under the hood
An Index Scan descends the index (a B-tree, by default) to the first matching entry, then walks the leaf level following the requested order. Each index entry points at a heap location, so for every match the executor also reads the table page to fetch the full row and check MVCC visibility.
That second step — the heap fetch — is the cost that matters. When matches are few, the fetches are few. When matches are many and scattered, you pay one (often random) page read per row, which is why a highly-matching Index Scan can lose to a Bitmap Heap Scan or even a plain Seq Scan.
Reading the numbers
Index Scan using orders_customer_idx on orders
(cost=0.43..38.20 rows=12 width=64) (actual time=0.03..0.09 rows=11 loops=1)
Index Cond: (customer_id = 42)
Buffers: shared hit=14
Index Condis what the index satisfied directly — good. Anything that shows up as a separateFilterwas not served by the index and was checked row by row.- Low
rowsand lowBuffersis the signature of a healthy lookup. - If you see a big
Filterwith manyRows Removed by Filter, the index found the rows but the predicate wasn't fully covered — consider a composite index.
Why the planner sometimes refuses your index
- Stale statistics — run
ANALYZE; the planner may think the table is tiny or the value common. - Non-sargable predicates —
WHERE date_trunc('day', ts) = $1can't use an index onts; index the expression or rewrite as a range. - Type mismatches — comparing a
bigintcolumn to anumericliteral can defeat the index. - Low selectivity — if the value matches a large fraction of rows, a scan is genuinely cheaper and the planner is right.
Index order and ORDER BY
An Index Scan returns rows in index order, so it can satisfy an ORDER BY for
free — no Sort node. Combined with LIMIT, that makes "latest N" queries almost
instant, if the index's column order and direction match the ORDER BY.
What the analyzer flags here
- Bad row estimate — wrong row counts can make this the wrong choice
Settings that influence it
random_page_costeffective_cache_size
FAQ
- Why does PostgreSQL ignore my index?
- Common reasons: stale statistics, a condition that isn't sargable (a function or type cast on the column), the query matching too many rows (a scan is cheaper), or the index column order not matching the predicate.