Parallel Index and Bitmap Scans #

A query over 400 million rows uses an index and runs single-threaded, while the same query forced to a sequential scan spreads across four workers and finishes sooner. Parallelism is not limited to sequential scans — but which index paths can use workers, and how the work is divided, differs enough between them that plans often end up serial by accident.

Worker mechanics are covered in parallel query execution. This page is about the index-based paths.

The Condition #

Four parallel-aware scan nodes exist for index access:

What stays serial:

The worker count is chosen from relation size, capped by max_parallel_workers_per_gather, and can be overridden per table.

What parallelises and how A table of scan types. Parallel sequential scans divide blocks among workers. Parallel B-tree index scans divide index pages. Parallel index-only scans do the same without heap access. Parallel bitmap heap scans build the bitmap in the leader and divide heap pages. GIN and GiST scans have no parallel variant. divided by notes Parallel Seq Scan table blocks any table Parallel Index Scan index pages B-tree only Parallel Index Only Scan index pages needs visibility map Parallel Bitmap Heap Scan heap pages bitmap built serially GIN / GiST scan not divided always serial a serial bitmap index scan can still feed a parallel heap scan

Annotated EXPLAIN Evidence #

A parallel bitmap heap scan:

EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM events WHERE occurred_at >= '2026-09-01' AND kind = 'purchase';
Finalize Aggregate  (actual time=2104.6..2104.6 rows=1 loops=1)
  ->  Gather  (actual rows=5 loops=1)
        Workers Planned: 4  Workers Launched: 4
        ->  Partial Aggregate  (actual rows=1 loops=5)
              ->  Parallel Bitmap Heap Scan on events  (actual rows=240822 loops=5)
                    Recheck Cond: (occurred_at >= '2026-09-01'::date)
                    Filter: (kind = 'purchase'::text)
                    Heap Blocks: exact=3841
                    ->  Bitmap Index Scan on events_occurred_at_idx  (actual time=182.4..182.4 rows=8408220 loops=1)
                          Index Cond: (occurred_at >= '2026-09-01'::date)
Execution Time: 2106.2 ms
-- note loops=1 on the Bitmap Index Scan: the bitmap was built once, by the leader
-- loops=5 on the heap scan: five processes divided the heap pages

A parallel index scan:

Finalize Aggregate  (actual time=812.4..812.4 rows=1 loops=1)
  ->  Gather  (actual rows=5 loops=1)
        Workers Planned: 4  Workers Launched: 4
        ->  Partial Aggregate  (actual rows=1 loops=5)
              ->  Parallel Index Only Scan using events_occurred_kind_idx on events
                    (actual rows=240822 loops=5)
                    Index Cond: ((occurred_at >= '2026-09-01'::date) AND (kind = 'purchase'::text))
                    Heap Fetches: 0
Execution Time: 814.1 ms
-- index pages divided among five processes; no heap access at all

And a serial one:

  ->  Bitmap Heap Scan on documents  (actual rows=41204 loops=1)
        Recheck Cond: (tsv @@ to_tsquery('billing & error'))
        ->  Bitmap Index Scan on documents_tsv_gin  (actual rows=41204 loops=1)
-- GIN: no parallel variant for the index scan, and the heap scan was not worth parallelising here
Reading loops in a parallel scan Three items are annotated. Loops of one on a bitmap index scan under a parallel heap scan shows the bitmap was built by the leader alone. Loops equal to participants on the heap scan shows the pages were divided. A parallel index only scan with loops equal to participants shows the index pages themselves were divided. Bitmap Index Scan … loops=1 bitmap built once, serially Parallel Bitmap Heap Scan … loops=5 heap pages divided Parallel Index Only Scan … loops=5 index pages divided loops under Gather counts participants, not repetitions

Step-by-Step Resolution #

  1. Check whether the scan is parallel-aware at all. The node name starts with Parallel when it is.

  2. If the plan is serial and the relation is large, check the usual blockers: parallel-unsafe functions, max_parallel_workers_per_gather = 0, cursors, or a write.

  3. For GIN-driven queries, accept a serial index scan and consider whether the heap scan above it is worth parallelising — it will be when many rows match and the recheck is expensive.

  4. Prefer a covering index for parallel index-only scans, which divide work cleanly and avoid heap access entirely.

  5. Adjust per-table worker counts where the planner is too conservative:

    ALTER TABLE events SET (parallel_workers = 8);
  6. Watch the bitmap build. When a parallel bitmap heap scan’s serial Bitmap Index Scan dominates the node’s time, parallelism buys little; a composite index that turns the query into a parallel index scan is the better fix.

  7. Measure with Workers Launched, not Workers Planned, since the pool may be exhausted.

Same aggregate, four paths A serial bitmap heap scan takes 4,104 milliseconds. A parallel bitmap heap scan with four workers takes 2,106 milliseconds, limited by the serial bitmap build. A parallel index only scan with four workers takes 814 milliseconds. A serial index only scan takes 3,120 milliseconds. serial bitmap scan 4,104 ms parallel bitmap scan 2,106 ms serial index only scan 3,120 ms parallel index only scan 814 ms the serial bitmap build limits the second bar's speedup

Before and After #

-- BEFORE: parallel bitmap heap scan, serial bitmap build dominating
Bitmap Index Scan (loops=1, 182 ms) → Parallel Bitmap Heap Scan (loops=5)    Execution Time: 2106.2 ms

-- AFTER: composite index enabling a parallel index-only scan
Parallel Index Only Scan using events_occurred_kind_idx (loops=5)  Heap Fetches: 0   Execution Time: 814.1 ms

Why the bitmap build stays serial #

A bitmap is a shared data structure representing the whole set of matching row locations; building it cooperatively would require coordination that costs more than it saves for most queries. PostgreSQL therefore has the leader build it, then shares it with workers for the heap phase, where the work is naturally divisible by page.

The consequence for tuning is a ceiling: in a parallel bitmap heap scan, the serial portion is the bitmap index scan, and Amdahl’s law applies. If that portion is 20% of the node’s time, four workers cannot make the node more than about three times faster. Reading the Bitmap Index Scan’s own actual time against the parent’s tells you the ceiling before you add workers.

The same reasoning applies to other partly serial plans. Aggregation splits well when workers reduce their input, as covered in partial and finalize aggregate in parallel plans; window functions do not split at all. Identifying the serial portion first prevents disappointment from adding workers to a plan that cannot use them.

Common Pitfalls #

Expecting GIN scans to parallelise. They cannot. Diagnostic signal: single-threaded full-text queries. Fix: parallelise the heap phase, or reduce matched rows.

Ignoring the serial bitmap build. It caps the speedup. Diagnostic signal: a large actual time on a loops=1 bitmap index scan. Fix: an index that supports a direct parallel index scan.

Reading per-loop rows as totals. rows=240822 loops=5 is 1.2 million. Diagnostic signal: apparent row counts five times too low. Fix: multiply by loops.

Small relations. Below the size thresholds no workers are considered. Diagnostic signal: serial plans on modest tables. Fix: accept it, or lower the thresholds deliberately for a workload.

Frequently Asked Questions #

Can index scans run in parallel in PostgreSQL? #

Yes, for B-tree indexes: Parallel Index Scan and Parallel Index Only Scan divide index pages among workers. GIN and GiST index scans have no parallel variant.

Is a Bitmap Index Scan parallel? #

No. In a Parallel Bitmap Heap Scan the leader builds the bitmap alone, and workers then divide the heap pages. The bitmap index scan itself shows loops=1 in the plan.

Why does my index scan not use parallel workers? #

Either the index type has no parallel variant, the relation is below the minimum parallel scan size, the query contains something parallel-unsafe, or the worker pool was exhausted at execution time.

Up: Parallel Query Execution