When the Planner Chooses Parallel Plans
Understand the cost thresholds and table sizes that trigger parallel scans and joins.
When the Planner Chooses Parallel Plans is a free PostgreSQL Performance & Query Optimization lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PostgreSQL Performance & Query Optimization learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Parallelism Is a Cost Decision
PostgreSQL does not run every query in parallel. The planner only generates a parallel plan when it believes the extra cost of spinning up workers will be repaid by faster execution.
- Workers cost money: the planner adds startup and tuple-transfer overhead for each one.
- Parallelism only pays off when the table is large enough and the work is CPU- or IO-heavy enough.
- Tiny tables and cheap queries stay sequential — the overhead would dominate.
In this lesson you will learn exactly which thresholds and table sizes flip the planner into a parallel plan.
The Three Gating GUCs
Three settings gate whether parallelism is even considered before any cost math runs:
- max_parallel_workers_per_gather — max workers a single Gather node may request (default 2). If 0, parallelism is disabled for that query.
- max_parallel_workers — cap across the whole instance (default 8).
- max_worker_processes — the hard OS-level ceiling on background workers (default 8).
If max_parallel_workers_per_gather = 0, the planner never produces a parallel path, regardless of table size.
SHOW max_parallel_workers_per_gather;
SHOW max_parallel_workers;
SHOW max_worker_processes;The Size Threshold: min_parallel_table_scan_size
The single most important table-size trigger is min_parallel_table_scan_size (default 8MB). A relation must be at least this large before the planner even considers a parallel sequential scan over it.
- For indexes, the analogous setting is min_parallel_index_scan_size (default
512kB). - Below the threshold, the relation is treated as too small to bother parallelizing.
So a 6MB table will almost never get a parallel scan, while a 1GB table easily clears the bar.
SHOW min_parallel_table_scan_size;
SHOW min_parallel_index_scan_size;How Worker Count Scales With Size
The number of workers is not fixed — it grows logarithmically with table size. Each time the relation triples past the threshold, the planner allows one more worker (capped by max_parallel_workers_per_gather).
- 8MB to 24MB: up to 1 worker
- 24MB to 72MB: up to 2 workers
- 72MB to 216MB: up to 3 workers
- Each step multiplies the previous boundary by 3.
This is why doubling a small table rarely changes the worker count, but going from 10MB to 1GB does.
The Per-Tuple and Setup Costs
Once a parallel path is possible, the planner weighs two cost penalties against the sequential plan:
- parallel_setup_cost (default
1000) — a one-time charge for launching workers and tearing down the shared memory. This is large, which is why tiny queries stay sequential. - parallel_tuple_cost (default
0.1) — charged per tuple passed from a worker up through the Gather node to the leader.
A parallel plan wins only when divided per-worker scan cost minus these penalties beats the sequential cost.
SHOW parallel_setup_cost;
SHOW parallel_tuple_cost;Reading a Parallel Plan in EXPLAIN
You confirm a parallel plan by looking for a Gather (or Gather Merge) node and Parallel-prefixed scan nodes underneath it.
Gathercollects rows from workers;Workers Plannedshows how many were requested.- Nodes below the Gather (e.g.
Parallel Seq Scan) run once per worker plus the leader. - Row counts under the Gather are per-worker estimates, not the total.
EXPLAIN (ANALYZE, VERBOSE)
SELECT count(*)
FROM orders
WHERE amount > 100;Forcing Parallelism for Testing
To observe a parallel plan on a smaller table during testing, lower the gating thresholds at the session level. This does not change the data — only the planner's willingness to parallelize.
- Set
min_parallel_table_scan_size = 0to remove the size gate. - Set
parallel_setup_cost = 0andparallel_tuple_cost = 0to remove the cost penalties. - Set
force_parallel_mode = on(renameddebug_parallel_queryin PG16+) to force a parallel path where one exists.
These are diagnostic knobs — never leave them on in production.
SET min_parallel_table_scan_size = 0;
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;
SET max_parallel_workers_per_gather = 4;Parallel-Aware vs Parallel-Restricted
Not every operation can run inside a parallel worker. The planner classifies pieces of the query:
- Parallel-safe — can run in a worker (most immutable/stable functions, plain scans).
- Parallel-restricted — must run only in the leader (e.g. references to temporary tables, correlated subplans, some CTEs).
- Parallel-unsafe — disables parallelism for the whole query (e.g. functions marked
PARALLEL UNSAFE, writing CTEs, sequence calls).
A single VOLATILE or PARALLEL UNSAFE function in the query can suppress an otherwise-parallel plan.
CREATE FUNCTION score(x int) RETURNS int
LANGUAGE sql IMMUTABLE PARALLEL SAFE
AS 'SELECT x * 2';Parallel Joins and Aggregates
Parallelism is not limited to scans. Once a driving relation is scanned in parallel, the planner can push joins and aggregation into the workers:
- Parallel Hash Join — workers cooperatively build one shared hash table.
- Parallel Nested Loop / Merge Join — each worker joins its slice of the outer relation.
- Partial Aggregate — each worker aggregates its rows, then a
Finalize Aggregateabove the Gather combines the partial results.
This is why a large GROUP BY over a big fact table is a classic parallel-plan candidate.
EXPLAIN
SELECT customer_id, count(*)
FROM orders
GROUP BY customer_id;Things That Quietly Disable Parallelism
Even on a huge table, several conditions block a parallel plan entirely:
- The query is run inside a cursor/
FETCHor withSELECT ... FOR UPDATErow locking. - It is a data-modifying statement on PG versions before 11 (later versions allow parallel
INSERT ... SELECT/ index builds, but not parallel UPDATE/DELETE execution). - It calls a
PARALLEL UNSAFEfunction or runs inside a function that has already claimed all workers. max_parallel_workers_per_gather = 0for the session.
When a plan you expect to be parallel is not, check these before tuning costs.
Putting the Thresholds Together
For the planner to pick a parallel plan, all of these must hold:
- Relation size at least
min_parallel_table_scan_size(8MB) for scans, or the index size threshold. max_parallel_workers_per_gather > 0and instance-wide worker budget available.- The query contains only parallel-safe (or at worst parallel-restricted) elements.
- The estimated parallel cost, after
parallel_setup_costandparallel_tuple_cost, beats the best sequential plan.
Miss any one and you get a sequential plan — usually for a good reason.
EXPLAIN (ANALYZE, BUFFERS)
SELECT sum(amount)
FROM orders
WHERE created_at >= DATE '2024-01-01';Quick Check
Test your understanding of the parallel-plan triggers.
Recap
Key takeaways on when PostgreSQL chooses a parallel plan:
- Parallelism is a cost decision gated first by size, then by cost math.
- min_parallel_table_scan_size (8MB) is the primary table-size trigger; min_parallel_index_scan_size (512kB) covers indexes.
- Worker count grows logarithmically — one more worker each time size triples — capped by
max_parallel_workers_per_gather. - parallel_setup_cost (1000) and parallel_tuple_cost (0.1) must be outweighed by the per-worker speedup.
- Parallel-unsafe functions, row locking, and a zero worker setting can disable parallelism even on huge tables.
- Confirm plans by looking for Gather / Partial Aggregate nodes in EXPLAIN.
Frequently asked questions
Is the “When the Planner Chooses Parallel Plans” lesson free?
Yes — the full text of “When the Planner Chooses Parallel Plans” is free to read here on the web, and the PostgreSQL Performance & Query Optimization course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PostgreSQL Performance & Query Optimization course, upgrade to CoddyKit PRO.
What will I learn in “When the Planner Chooses Parallel Plans”?
Understand the cost thresholds and table sizes that trigger parallel scans and joins. You practise PostgreSQL Performance & Query Optimization with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start PostgreSQL Performance & Query Optimization?
No prior experience is required. PostgreSQL Performance & Query Optimization on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “When the Planner Chooses Parallel Plans” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this PostgreSQL Performance & Query Optimization lesson?
Yes. Every PostgreSQL Performance & Query Optimization lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- When the Planner Chooses Parallel Plans
- Tuning Worker Counts and Gather Costs
- Parallel Aggregation and Hash Joins
- Diagnosing Why Parallelism Was Disabled