#postgres #postgres#internals

Five types of scans every backend dev should recognise

Seq scan, index scan, index-only scan, bitmap heap scan, TID scan. When the planner picks each, and what it means when you see the wrong one.

AD
Admin
DevDash Editor
Published
Apr 21, 2026
Read time
11 min
Difficulty
Intermediate
Five types of scans every backend dev should recognise

Reading a Postgres EXPLAIN output is like reading a flight itinerary — the destination is obvious, but you need to understand each leg before you can say whether the routing makes sense. Scan types are the first leg. There are five of them, and each one tells a different story.

1. Sequential scan (Seq Scan)

A sequential scan reads every page of a table in disk order. It's the baseline — no index, no shortcuts.

SQL
Seq Scan on orders  (cost=0.00..4821.00 rows=200000 width=48)
  Filter: (status = 'pending')

When it's correct: The planner chooses a seq scan when the filter is non-selective — if you're fetching more than 10–20% of the table, walking the heap is faster than bouncing between an index and heap pages. This is especially true on small tables.

When it's a problem: A seq scan on a large table with a highly selective filter (returning fewer than 1% of rows) means either no index exists or the planner's statistics are stale. Run ANALYZE and re-check.

2. Index scan

An index scan reads the index to find matching row locations (TIDs), then fetches each row from the heap.

SQL
Index Scan using orders_status_idx on orders
  (cost=0.43..8.45 rows=1 width=48)
  Index Cond: (status = 'pending' AND created_at > '2026-01-01')
Note

Each heap fetch in an index scan is a random I/O. On spinning disks this matters a lot; on NVMe it's nearly free. Keep that in mind when interpreting costs on cloud instances.

3. Index-only scan

If the index contains all columns the query needs, Postgres never has to touch the heap. This is the fastest scan type for selective queries on well-designed indexes.

SQL
Index Only Scan using orders_covering_idx on orders
  (cost=0.43..4.45 rows=1 width=16)
  Index Cond: (merchant_id = 42)
  Heap Fetches: 0

Heap Fetches: 0 is the number you want. If it's non-zero, your visibility map isn't current — run VACUUM.

4. Bitmap heap scan

A bitmap heap scan runs an index scan to collect all matching TIDs into a bitmap, then reads the heap in page order. It's the hybrid — more efficient than index scan for moderately selective queries.

SQL
Bitmap Heap Scan on orders  (cost=47.50..1842.30 rows=3200 width=48)
  Recheck Cond: (status = 'pending')
  ->  Bitmap Index Scan on orders_status_idx
        (cost=0.00..46.70 rows=3200 width=0)

You'll often see BitmapAnd or BitmapOr when the planner combines two indexes to answer a query — a technique that would be impossible with a plain index scan.

5. TID scan

A TID (tuple identifier) scan fetches rows by their physical location directly. You'll almost never write queries that trigger this — it's mostly used internally for CTID-based operations like CLUSTER or VACUUM.

SQL
Tid Scan on orders  (cost=0.00..4.01 rows=1 width=48)
  TID Cond: (ctid = '(0,1)'::tid)

Cheat sheet

Scan Best for Watch out for
Seq Scan Low selectivity, small tables Large tables + high selectivity
Index Scan High selectivity, random access Many rows → many random I/Os
Index-Only Scan Covering indexes Heap Fetches non-zero means stale visibility map
Bitmap Heap Scan Moderate selectivity Lossy bitmaps on very large result sets
TID Scan Internal ops N/A for application queries

The next time you see an unexpected scan type in an EXPLAIN, check selectivity first. If selectivity is fine, check statistics freshness. If that's fine, check your index definition.

AD
Written by
Admin
DevDash Editor

Deep technical writing on databases, performance engineering, and the infrastructure that keeps production running. All articles are reviewed and published by the DevDash editorial team.