pgconfigurator
pgconfigurator

Bitmap Heap Scan

Appears in EXPLAIN asBitmap Heap Scan

Reads heap pages in physical order from a bitmap of matching rows.

What it is

A Bitmap Heap Scan consumes a bitmap of matching tuple locations (built by one or more Bitmap Index Scans) and then visits the heap once per page, in physical order — turning scattered index lookups into a sequential-ish read.

When the planner picks it

For medium-selectivity queries that match more rows than a plain Index Scan handles well, or when combining several indexes with AND/OR.

Is it good or bad?

Often the sweet spot between Index Scan and Seq Scan. Watch for 'lossy' blocks: if work_mem is too small to hold an exact bitmap, PostgreSQL stores whole blocks instead and rechecks every row in them.

In depth

How it works under the hood

A bitmap plan runs in two phases. First, one or more Bitmap Index Scans read indexes and build an in-memory bitmap of which rows match — really a bitmap of tuple locations. If several indexes are involved they're combined with BitmapAnd / BitmapOr. Then the Bitmap Heap Scan walks that bitmap and reads each needed heap page once, in physical order.

That ordering is the whole point: it converts the scattered, random heap I/O of a large Index Scan into a single forward pass over the table — close to sequential-scan efficiency, but touching only the pages that contain matches.

Exact vs lossy — the number that bites

The bitmap lives in work_mem. If it doesn't fit, PostgreSQL degrades from a per-row bitmap to a per-block bitmap: it remembers "this 8 KB block has a match somewhere" and then rechecks every row in that block when it reads it.

Bitmap Heap Scan on big  (actual time=... rows=... loops=1)
  Recheck Cond: (val < 5000)
  Rows Removed by Index Recheck: 1240221
  Heap Blocks: exact=674 lossy=6724

lossy=6724 against exact=674 means most blocks are lossy — the bitmap blew its memory budget — and Rows Removed by Index Recheck shows the wasted recheck work. The fix is straightforward:

SET LOCAL work_mem = '128MB';  -- enough to hold an exact bitmap

When you'll see it

  • Medium-selectivity predicates: too many rows for a tidy Index Scan, too few to justify reading the whole table.
  • OR across indexed columns (via BitmapOr) and multi-column AND (via BitmapAnd) where no single composite index exists.

Tuning levers

  • work_mem — the direct cure for lossy bitmaps.
  • effective_cache_size / random_page_cost — shape whether the planner prefers a bitmap over an Index Scan or Seq Scan in the first place.
  • A composite index can sometimes replace a BitmapAnd of two single-column indexes with one cheaper Index Scan.

What the analyzer flags here

  • Lossy bitmap — work_mem too small, so rows are rechecked block by block

Paste a plan into the analyzer →

Settings that influence it

work_memeffective_cache_sizerandom_page_cost

How we tune these →

FAQ

What does 'lossy' mean in a bitmap heap scan?
When the precise per-row bitmap doesn't fit in work_mem, PostgreSQL degrades to a per-block bitmap. It then has to recheck the condition on every row of those blocks. Raising work_mem restores the exact, faster bitmap.

See also