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.
ORacross indexed columns (via BitmapOr) and multi-columnAND(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
Settings that influence it
work_memeffective_cache_sizerandom_page_cost
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.