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:
Parallel Index Scan— B-tree only. Workers cooperate by claiming index pages, each descending and scanning a portion. Available for ordered scans too, feedingGather Merge.Parallel Index Only Scan— the same, without heap access when the visibility map allows it.Parallel Bitmap Heap Scan— the bitmap is built by the leader alone, then workers divide the heap pages between them. TheBitmap Index Scanbeneath it is therefore serial, whatever the index type.Parallel Seq Scan— for completeness; workers claim blocks of the table.
What stays serial:
- GIN and GiST index scans have no parallel-aware variants, so a query driven by one runs its scan in a single process — though a bitmap heap scan above it can still parallelise the heap access.
- Any plan containing a parallel-unsafe function, a cursor, a writing statement or a locking clause is excluded from parallelism entirely, as covered in why parallel workers are not launched.
- Small scans. The planner requires the relation to exceed
min_parallel_index_scan_size(default 512 kB) for index scans andmin_parallel_table_scan_size(8 MB) for table scans before considering workers.
The worker count is chosen from relation size, capped by max_parallel_workers_per_gather, and can be overridden per table.
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
Step-by-Step Resolution #
-
Check whether the scan is parallel-aware at all. The node name starts with
Parallelwhen it is. -
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. -
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.
-
Prefer a covering index for parallel index-only scans, which divide work cleanly and avoid heap access entirely.
-
Adjust per-table worker counts where the planner is too conservative:
ALTER TABLE events SET (parallel_workers = 8); -
Watch the bitmap build. When a parallel bitmap heap scan’s serial
Bitmap Index Scandominates the node’s time, parallelism buys little; a composite index that turns the query into a parallel index scan is the better fix. -
Measure with
Workers Launched, notWorkers Planned, since the pool may be exhausted.
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.
Related #
- Parallel Query Execution — parent guide: worker mechanics
- Why Parallel Workers Are Not Launched — the blockers
- Parallel Seq Scan Size Thresholds — how worker counts are chosen