DB Internals AtlasPostgreSQL 18.6 Home About

Query processing

Parallel query

For big scans PostgreSQL can start parallel worker processes. They share the work block by block, each produces a partial result, and a Gather node in the leader combines them.

In plain words

Normally one query is handled by one process, using one CPU core. For a big table that can be slow even when the server has idle cores. Parallel query lets the backend (the leader) start a few helper processes, parallel workers, that read different parts of the same table at the same time.

Each helper does part of the job, for example counting its share of rows, and hands a partial result to the leader. The Gather node collects them and the leader finishes the job (adds up the partial counts). Two workers plus the leader can make a scan almost three times faster.

It is not free: starting workers takes time, so PostgreSQL only uses parallelism for big enough tables and expensive enough queries, and not for most data-changing statements.

For DBAs and operators

Plan shapes (real PostgreSQL 18 output)

-- EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, BUFFERS OFF)
Finalize Aggregate (actual rows=1.00 loops=1)
  ->  Gather (actual rows=3.00 loops=1)
        Workers Planned: 2
        Workers Launched: 2
        ->  Partial Aggregate (actual rows=1.00 loops=3)
              ->  Parallel Seq Scan on o (actual rows=666666.67 loops=3)

loops=3 is two workers plus the leader; per-loop numbers are averages. Gather Merge is used when order matters: each process sorts its share and the leader merges the sorted streams:

Gather Merge  (cost=22755.12..25025.40 rows=19493 width=14)
  Workers Planned: 2
  ->  Sort  (cost=21755.09..21775.40 rows=8122 width=14)
        Sort Key: amt
        ->  Parallel Seq Scan on o  (cost=0.00..21227.67 rows=8122 width=14)
              Filter: (amt > '990'::numeric)

Parallel-aware nodes: Parallel Seq Scan (blocks handed out in chunks), Parallel Index / Index Only Scan (B-tree), Parallel Bitmap Heap Scan, Parallel Hash Join (one shared hash table, enable_parallel_hash), Parallel Append. Aggregates split into Partial and Finalize when the aggregate supports combining.

How many workers

The planner sizes workers by table size: 1 worker at min_parallel_table_scan_size (8MB), +1 each time the table is three times larger, capped by max_parallel_workers_per_gather (2); the table's parallel_workers storage parameter replaces the size-based calculation. At run time workers come from max_parallel_workers (8) inside max_worker_processes (8); if none are free the query still runs, with Workers Launched lower than planned; with 0 the leader runs the whole plan below the Gather by itself.

When parallelism is not used

  • The query writes (except the SELECT part of CREATE TABLE … AS, SELECT INTO, CREATE MATERIALIZED VIEW and REFRESH MATERIALIZED VIEW), uses FOR UPDATE, or runs inside a cursor / PL/pgSQL FOR loop that may be suspended.
  • It calls a PARALLEL UNSAFE function (the default for user functions!), or runs inside another query that is already parallel.
  • The estimated gain does not beat parallel_setup_cost (1000) + parallel_tuple_cost (0.1 per row passed to the leader).
  • Temporary tables cannot be scanned in parallel.

Each worker gets its own work_mem per sort/hash, so a parallel plan can use several times more memory. debug_parallel_query = on adds a Gather to the top of every plan where that appears safe, for testing parallel safety only.

SELECT pid, leader_pid, backend_type, query
  FROM pg_stat_activity WHERE backend_type = 'parallel worker';

For developers: inside the source

Where it lives

  • README.parallel: the design notes (what state is copied to workers, what is forbidden in parallel mode).
  • parallel.c: generic infrastructure: EnterParallelMode(), CreateParallelContext(), InitializeParallelDSM() (a DSM segment with a shm_toc holding GUCs, snapshots, combo CIDs, transaction state), LaunchParallelWorkers() (registers dynamic background workers whose entry point is ParallelWorkerMain()), WaitForParallelWorkersToFinish(), DestroyParallelContext().
  • execParallel.c: query-specific part. ExecInitParallelPlan() serializes the plan (ExecSerializePlan(), stored under PARALLEL_KEY_PLANNEDSTMT) and calls ExecParallelInitializeDSM() so each parallel-aware node can set up shared state (e.g. ExecSeqScanInitializeDSM()); workers run ParallelQueryMain().
  • nodeGather.c, nodeGatherMerge.c (a binary heap merges sorted worker streams), tqueue.c over shm_mq.c.

Call flow

  1. On its first call ExecGather() runs ExecInitParallelPlan(), LaunchParallelWorkers() and ExecParallelCreateReaders().
  2. Each worker: ParallelWorkerMain() restores the leader's state, then ParallelQueryMain() runs the plan below Gather and sends tuples through CreateTupleQueueDestReceiver() (shm_mq_send()).
  3. The leader's gather_getnext() reads with TupleQueueReaderNext() and, when parallel_leader_participation is on, also runs the subplan itself (need_to_scan_locally).
  4. Parallel Seq Scan blocks come from table_block_parallelscan_nextpage() (tableam.c): an atomic fetch-and-add on a shared counter. table_block_parallelscan_startblock_init() sizes chunks so the table splits into about PARALLEL_SEQSCAN_NCHUNKS (2048) pieces, at most 8192 blocks each, and shrinks them near the end.

Planner side

compute_parallel_worker() implements the "×3 per extra worker" rule; generate_useful_gather_paths() adds Gather/Gather Merge on top of partial paths; cost_gather() charges parallel_setup_cost and parallel_tuple_cost; get_parallel_divisor() counts the leader as 1 − 0.3 × workers of a process (nothing from 4 workers on). Parallel safety comes from max_parallel_hazard() / is_parallel_safe() in clauses.c, which read pg_proc.proparallel.

Observe and extend

  • debug_parallel_query = on (or regress, which also hides the forced Gather from EXPLAIN) runs the test suite with workers; see src/test/regress/sql/select_parallel.sql and write_parallel.sql.
  • Wait events: BgworkerStartup, ExecuteGather, MessageQueueReceive, ParallelFinish.
  • Extensions can use the same API: CreateParallelContext(library_name, function_name, nworkers) runs your own function in workers. Custom scan providers become parallel-aware through the DSM callbacks in CustomExecMethods.

Parameters that shape this

max_parallel_workers_per_gather max_parallel_workers max_worker_processes parallel_setup_cost parallel_tuple_cost min_parallel_table_scan_size min_parallel_index_scan_size parallel_leader_participation enable_parallel_hash enable_parallel_append debug_parallel_query

Related topics

Parallel and background workers Planner cost model Scan methods Join methods Life of a query JIT compilation