Cookbook recipe

Bitmap Heap Scan Rechecking Excessive Rows (Lossy Bitmap)

Applies to PostgreSQL 13–17 Last reviewed May 2026 Grounded in source
Estimated investigation4 min

Scenario

Scenario A product search query uses a composite index and returns about 500 rows, but runs for 8 seconds. EXPLAIN (ANALYZE, BUFFERS) shows Heap Blocks: exact=20 lossy=4800 and Rows Removed by Index Recheck: 982,000. An engineer…

Investigation Path

Scenario

A product search query uses a composite index and returns about 500 rows, but runs for 8 seconds. EXPLAIN (ANALYZE, BUFFERS) shows Heap Blocks: exact=20 lossy=4800 and Rows Removed by Index Recheck: 982,000. An engineer wrongly suspects the index is corrupt. The actual cause is that work_mem is too low for the bitmap to fit in memory, causing the planner to switch to a lossy (page-granularity) bitmap.

How to Identify

Conditions:

  • EXPLAIN (ANALYZE, BUFFERS) output shows Heap Blocks: exact=X lossy=Y where lossy count is high
  • Rows Removed by Index Recheck: counter is very high relative to returned rows
  • work_mem is set very low (64kB–1MB)
  • A Bitmap Index Scan + Bitmap Heap Scan is in the plan (not a plain Index Scan)
  • Table has many rows and the bitmap for matching pages exceeds work_mem

Analysis Steps

-- 1. Run EXPLAIN with BUFFERS to see exact vs lossy bitmap blocks
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM products WHERE category = 'electronics' AND in_stock = true;

-- Look for in the output:
--   Bitmap Heap Scan on products
--     Recheck Cond: (...)
--     Rows Removed by Index Recheck: <high number>
--     Heap Blocks: exact=X lossy=Y    <- lossy=Y is the problem

-- 2. Check current work_mem
SHOW work_mem;

-- 3. Understand why lossy bitmaps occur:
-- Each page needs 1 bit in exact mode.
-- If total pages > (work_mem * 8), bitmap switches to lossy (1 bit per page, no row tracking)
-- In lossy mode: every row on a lossy page must be rechecked against the predicate
SELECT
    pg_size_pretty(pg_relation_size('products'))    AS table_size,
    pg_relation_size('products') / 8192             AS total_pages,
    current_setting('work_mem')                     AS current_work_mem;

-- 4. Estimate bitmap size needed for exact mode:
-- exact_bitmap_bytes = total_pages / 8
-- If exact_bitmap_bytes > work_mem → lossy

-- 5. Check effective_io_concurrency (controls prefetch for Bitmap Heap Scan)
SHOW effective_io_concurrency;

Pitfalls

  • Lossy bitmaps are a work_mem problem, NOT an index problem — do not drop or rebuild the index
  • Increasing work_mem affects all concurrent queries; each connection can allocate work_mem per sort/hash operation — use SET LOCAL work_mem in a transaction for targeted sessions
  • A plain Index Scan is different from a Bitmap Index Scan + Bitmap Heap Scan — bitmaps combine results from multiple indexes; lossy mode only affects bitmap scans
  • effective_io_concurrency should be 200 for SSDs, 1-4 for spinning disks — it controls how aggressively PostgreSQL prefetches bitmap heap blocks
  • A composite index on the exact filtered columns can eliminate the bitmap scan entirely by making a plain Index Scan viable

Resolution Approach

Lossy bitmaps occur when the in-memory bitmap for matching pages exceeds work_mem. The fix is not about the index — it is about memory:

This is a Pro lesson

Get every Learning Pathway and cookbook recipe — grounded in PostgreSQL source code, with diagnostics, fixes, and prevention for each topic.

Continue this lesson to learn:

  • Mitigation Actions
  • All 36 Learning Pathway lessons
  • 170+ cookbook recipes
  • Source-grounded diagnostics & fixes

Secure checkout Cancel anytime Source-grounded

Career Impact

This scenario builds production judgment and operational confidence under pressure.

Open Career Dashboard →

Keep going

Related & next steps

Was this helpful?

← All cookbook recipes