Skip to content

并行配置参数(Parallel Configuration) ​

并行查询的开关不在 SQL 语句里,而在服务器参数(GUC,Grand Unified Configuration)里。规划器要不要并行、能并行多少、每个 worker 能占多少内存,全部由一组参数共同决定。理解这组参数,才能回答「为什么我的查询没并行」和「并行之后为什么内存涨了」这两个最常见的问题。

本节逐一梳理控制并行行为的参数:进程池类的 max_worker_processes、max_parallel_workers、max_parallel_workers_per_gather,成本类的 parallel_setup_cost、parallel_tuple_cost,规模阈值类的 min_parallel_table_scan_size、min_parallel_index_scan_size,以及表级选项 parallel_workers 与并行安全标记。所有默认值均以 PostgreSQL 18 为准。

前置知识

阅读本节前,建议先了解:并行查询原理

基础概念 ​

四层「漏斗」:worker 数量被谁限制 ​

一条查询最终能启动几个 worker,要依次穿过四道闸门,取四者中的最小值:

text
max_worker_processes                    整个实例的 background worker 总池子
        │                                (并行、逻辑复制、autovacuum 一起抢)
        ▼
max_parallel_workers                    池子里留给「并行操作」的上限
        │
        ▼
max_parallel_workers_per_gather         单条查询(单个 Gather 节点)的上限
        │
        ▼
表/索引上声明的 parallel_workers        对象级的显式覆盖(若已设置)
        │
        ▼
实际启动的 worker 数(Workers Launched)

关键点:这是「与」关系,不是「或」关系。任何一层设小了,其余层设多大都无效。

三类参数,三种用途 ​

类别参数作用
进程池类max_worker_processes、max_parallel_workers、max_parallel_workers_per_gather、max_parallel_maintenance_workers决定「能并行到多大规模」与「并发时会不会互相挤」
成本与阈值类parallel_setup_cost、parallel_tuple_cost、min_parallel_table_scan_size、min_parallel_index_scan_size决定规划器「值不值得」选并行计划
行为与安全类parallel_leader_participation、debug_parallel_query、enable_parallel_append、enable_parallel_hash、PARALLEL SAFE/RESTRICTED/UNSAFE决定「谁来干活」「哪些计划形状允许」

参数的可修改性 ​

不同参数能改的时机不同,这直接决定了运维方式:

生效方式参数说明
仅需重载配置(pg_reload_conf() / reload)max_parallel_workers、max_parallel_workers_per_gather、max_parallel_maintenance_workers、parallel_leader_participation、parallel_setup_cost、parallel_tuple_cost、各类 enable_*改 postgresql.conf 后 reload 即可
必须重启实例max_worker_processes池子大小在启动时固定
可在会话内 SET成本类、阈值类、enable_*、max_parallel_workers_per_gather适合做临时对照实验

重要:max_worker_processes 需要重启。生产库上想「立刻」增加并行度,只能先看这一项有没有余量。

PostgreSQL 18 的重要变化 ​

有三处与并行配置直接相关的变化,必须知道:

  1. force_parallel_mode 已更名为 debug_parallel_query。 PostgreSQL 16 起官方把该参数由 force_parallel_mode 更名为 debug_parallel_query(枚举型,取值 off / on / regress,默认 off),它属于 §19.17 Developer Options,官方明确要求「不应在生产数据库上使用」。PostgreSQL 18 文档中只保留新名,旧名不应再出现在任何配置里。
  2. 异步 I/O(AIO,Asynchronous I/O)落地。 并行顺序扫描、位图堆扫描可以借助 AIO 提升吞吐,相关旋钮是 io_method 与 effective_io_concurrency(后者默认 16)。
  3. 监控字段补齐。 pg_stat_database 新增 parallel_workers_to_launch 与 parallel_workers_launched,可以量化「规划了却没启动」的 worker 比例。

语法与用法 ​

查看当前值 ​

sql
-- 一次性查看所有并行相关参数
SELECT name, setting, unit, context, pending_restart
FROM pg_settings
WHERE name IN (
    'max_worker_processes',
    'max_parallel_workers',
    'max_parallel_workers_per_gather',
    'max_parallel_maintenance_workers',
    'parallel_leader_participation',
    'parallel_setup_cost',
    'parallel_tuple_cost',
    'min_parallel_table_scan_size',
    'min_parallel_index_scan_size'
)
ORDER BY name;

pg_settings 的三列非常有用:

  • setting:当前生效值(会话内可能已被 SET 覆盖)。
  • context:可修改时机,postmaster 表示需重启,user 表示普通用户可以 SET。
  • pending_restart:是否「已改配置但还没重启」。

单参数确认 ​

sql
SHOW max_worker_processes;              -- 8(默认)
SHOW max_parallel_workers;              -- 8(默认)
SHOW max_parallel_workers_per_gather;   -- 2(默认)

临时改(会话级) ​

sql
-- 只影响当前会话,事务结束/断连后自动还原
SET max_parallel_workers_per_gather = 4;
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;

-- 事务级,提交或回滚即失效
BEGIN;
SET LOCAL max_parallel_workers_per_gather = 0;
EXPLAIN (ANALYZE) SELECT count(*) FROM big_table;
COMMIT;

持久改(实例级) ​

sql
-- 写入 postgresql.auto.conf,reload 生效
ALTER SYSTEM SET max_parallel_workers_per_gather = 4;
SELECT pg_reload_conf();

-- 需要重启的参数
ALTER SYSTEM SET max_worker_processes = 24;
-- 然后重启实例;在此之前 pg_settings.pending_restart = true

对象级改(表级) ​

sql
-- 只影响针对这张表的扫描;0 表示禁止并行
ALTER TABLE fact_orders  SET (parallel_workers = 6);
ALTER TABLE dim_currency SET (parallel_workers = 0);

-- 查询现有设置
SELECT relname, reloptions
FROM pg_class
WHERE reloptions IS NOT NULL
  AND array_to_string(reloptions, ',') LIKE '%parallel_workers%';

函数与聚合的并行标记 ​

sql
-- 函数:默认 UNSAFE,需要显式声明
CREATE OR REPLACE FUNCTION my_pure_func(integer)
RETURNS integer
LANGUAGE sql
PARALLEL SAFE
AS $$ SELECT $1 * 2 $$;

-- 查看现有标记(u=UNSAFE, r=RESTRICTED, s=SAFE)
SELECT n.nspname, p.proname, p.proparallel
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY 3, 1, 2;

详细说明 ​

max_worker_processes:总池子,需重启 ​

max_worker_processes(integer,默认 8)设置系统能支持的 background worker 进程总数上限。这个池子不只服务并行查询,还被以下功能共同占用:

  • 并行查询的 worker
  • 逻辑复制(logical replication)的 apply worker
  • autovacuum 的 worker
  • 用户自定义的 background worker(扩展)
  • pg_stat_statements 之外,各扩展自行注册的 worker

因此它不是「并行专用配额」。把池子设得过小,最先受影响的往往是并行查询——因为并行是「随时可能来、随时可能走」的临时需求,抢不过长期驻留的复制 worker。

它只能通过重启生效,因为池子大小在 postmaster 启动时就要确定(要预留共享内存中的进程槽位)。

sql
-- 检查池子的实际占用情况(PG18)
SELECT count(*) FILTER (WHERE backend_type = 'parallel worker')  AS parallel_workers,
       count(*) FILTER (WHERE backend_type LIKE '%logical replication%') AS logical_workers,
       count(*) FILTER (WHERE backend_type = 'autovacuum worker') AS autovacuum_workers,
       current_setting('max_worker_processes')::int              AS pool_size
FROM pg_stat_activity;

max_parallel_workers:留给并行的上限 ​

max_parallel_workers(integer,默认 8)设置任意时刻可以同时活动的并行 worker 总数。它是「池子中划给并行的那一块」。

和 max_worker_processes 的区别:后者是「服务器一共能养多少 worker」,前者是「其中允许多少个同时在为并行查询干活」。如果 max_worker_processes = 8 而 max_parallel_workers = 8,意味着并行可以吃满整个池子,此时逻辑复制就可能拿不到 worker。

规划器在估算时会参考这一项:当它认为当前活跃并行 worker 已接近上限时,可能降低并行度。

max_parallel_workers_per_gather:单查询并行度 ​

max_parallel_workers_per_gather(integer,默认 2)是最常用、也最该谨慎调整的一个。它限制单个 Gather(或 Gather Merge)节点能启动的 worker 数,也就是「一条查询最多开几个并行 worker」。

注意「每个 Gather 节点」这个措辞:一个计划里如果有多个 Gather(较少见,如并行子查询各自 Gather),每个都可以用到这个数。但常规查询只有顶层一个。

默认值 2 是刻意保守的。原因是内存放大:

text
单个并行节点最坏内存占用 ≈ work_mem × hash_mem_multiplier
整条查询最坏内存占用     ≈ hash_mem_multiplier × work_mem × (worker 数 + 1)

若 work_mem = 64MB、hash_mem_multiplier = 2.0、max_parallel_workers_per_gather = 4,一个 Hash Join 节点最坏可达 2.0 × 64MB × (4 + 1) = 640MB——而且是每个并发查询各自这么多。

sql
-- 会话级验证:同一条查询在 2 个与 4 个 worker 下的耗时
SET max_parallel_workers_per_gather = 2;
EXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM fact_orders WHERE created_at >= '2026-01-01';

SET max_parallel_workers_per_gather = 4;
EXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM fact_orders WHERE created_at >= '2026-01-01';

max_parallel_maintenance_workers:维护命令的并行度 ​

max_parallel_maintenance_workers(integer,默认 2)限制单条维护类命令能启动的并行 worker 数。受它影响的命令包括:

  • CREATE INDEX(构建 B-tree、GIN 或 BRIN 索引时可以并行)
  • VACUUM(仅不带 FULL 时)

它和 max_parallel_workers_per_gather 是两套独立预算:前者管「维护」,后者管「查询」。两者都从 max_parallel_workers 这个总配额里取。

sql
-- 让大表建索引用上更多 worker(仅当前会话)
SET max_parallel_maintenance_workers = 4;
CREATE INDEX idx_orders_created ON orders (created_at);

-- 查看本次会话的并行度设置,配合日志确认实际启动了几个 worker
SHOW max_parallel_maintenance_workers;

注意:普通 CREATE INDEX 可以并行,CREATE INDEX CONCURRENTLY 不能——非阻塞建索引要求单进程推进多阶段状态,无法拆分给多个 worker。

parallel_leader_participation:leader 要不要一起干 ​

parallel_leader_participation(boolean,默认 on)决定 leader 进程在执行 Gather / Gather Merge 时,是否也去执行 Gather 下面的并行部分。

  • on(默认):leader 也参与扫描与计算,最后再汇总。多数场景吞吐更好,因为「N 个 worker + 1 个 leader」比「N 个 worker 等结果」更充分。
  • off:leader 只负责汇总与读取元组,不参与并行部分。适合 leader 需要尽快响应(例如要处理 LIMIT 提前终止)或 leader 本身较忙的场景。
sql
-- 关掉 leader 参与后,实际并行度下降,但 leader 更「专注」
SET parallel_leader_participation = off;
EXPLAIN (ANALYZE, VERBOSE) SELECT count(*) FROM fact_orders;

注意:off 不会让 Workers Planned 变化,它影响的是「leader 是否也在干活」,在 per-worker 统计里体现为 leader 自身的工作量下降。

parallel_setup_cost 与 parallel_tuple_cost:规划器的取舍旋钮 ​

这两个参数决定规划器认为并行「值不值得」:

参数默认值含义
parallel_setup_cost1000启动并行 worker 的固定代价(进程启动、内存初始化、状态共享等)
parallel_tuple_cost0.1从 worker 向 leader 传递每一行的代价

parallel_tuple_cost 是按行计费的,这一点极其关键。如果并行部分产出上千万行,光是「搬回 leader」的代价就有 0.1 × 1e7 = 1e6,足以让规划器放弃并行。这正是「分组数接近输入行数」的聚合不适合并行的原因——Finalize Aggregate 要接收的行数太多。

诊断用法:如果确信并行更快但规划器没选,可以临时把两者清零,看规划器是否只是被成本吓住了。

sql
-- 诊断:清零后看是否会选并行计划
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;
EXPLAIN SELECT count(*) FROM fact_orders;
RESET parallel_setup_cost;
RESET parallel_tuple_cost;

如果清零后仍然没有并行计划,说明问题不在成本,而在「这个查询根本不允许并行」(见 §15.2 与 §15.4)。

min_parallel_table_scan_size 与 min_parallel_index_scan_size:规模门槛 ​

规划器不会为小表生成并行计划——启动开销划不来。门槛由两个参数控制:

参数默认值含义
min_parallel_table_scan_size8MB表需要超过这个大小,才考虑并行顺序扫描
min_parallel_index_scan_size512kB索引需要超过这个大小,才考虑并行索引扫描

这两个是下限,不是「只要超过就一定并行」。它们和成本参数配合工作:表够大只是「有资格」,还得成本划算才会被选中。

自动推断的 worker 数也与表大小挂钩:表越大,规划器给的 Workers Planned 越多(在 max_parallel_workers_per_gather 之内)。粗略关系是表大小每翻一倍,并行度可增加一级。

sql
-- 看一张表的物理大小,判断是否够格并行
SELECT pg_size_pretty(pg_relation_size('fact_orders')) AS table_size,
       pg_size_pretty(pg_total_relation_size('fact_orders')) AS total_size;

表级 parallel_workers:绕开推断,直接指定 ​

ALTER TABLE ... SET (parallel_workers = N) 是对象级(storage parameter)设置,它显式覆盖规划器的自动推断:

  • N = 0:禁止并行扫描这张表。
  • N > 0:希望使用的 worker 数上限(仍受 max_parallel_workers_per_gather 约束,取较小值)。

它比全局调大 max_parallel_workers_per_gather 更精细:只让少数真正的大事实表用更多 worker,小表不受影响。

sql
-- 大型事实表:多用 worker
ALTER TABLE fact_orders SET (parallel_workers = 6);

-- 小而热的维表:禁止并行,避免无谓开销
ALTER TABLE dim_currency SET (parallel_workers = 0);

-- 恢复自动推断
ALTER TABLE fact_orders RESET (parallel_workers);

并行安全标记:SAFE / RESTRICTED / UNSAFE ​

函数、聚合、以及部分操作会被打上并行安全级别,它决定「能否出现在并行部分」:

级别pg_proc.proparallel语义
PARALLEL SAFEs可以安全地在 worker 中执行,不受并行上下文影响
PARALLEL RESTRICTEDr只能由 leader 执行;出现在并行部分会「截断」并行范围
PARALLEL UNSAFEu绝对不能出现在并行计划中

用户自定义函数默认是 PARALLEL UNSAFE。 这是最常见的「查询无法并行」原因:函数体内只是简单计算,但因为没标 SAFE,整个查询被降级为串行。

永久属于 PARALLEL RESTRICTED 的典型操作:扫描 CTE、扫描临时表、执行 DECLARE CURSOR。它们不会报错,只会静默地让并行范围缩小。

sql
-- 错误的标记会直接导致并行查询报错
CREATE FUNCTION bad_func() RETURNS int
LANGUAGE plpgsql PARALLEL SAFE      -- 错误!函数内会写表,必须保持 UNSAFE
AS $$ BEGIN INSERT INTO audit_log(msg) VALUES ('x'); RETURN 1; END $$;

debug_parallel_query:测试用,不要上生产 ​

PostgreSQL 16 删除了 force_parallel_mode。PostgreSQL 18 中对应的开发者选项是 debug_parallel_query(enum,默认 off),属于 §19.17 Developer Options:

取值行为
off只在预期能提升性能时才用并行(正常生产行为)
on对所有「看起来安全」的查询强制加上 Gather 节点,让查询在并行 worker 中运行
regress同 on,另外为回归测试调整输出(抑制 worker 的上下文行、在 EXPLAIN 中隐藏 Gather)

它的用途只有一个:测试并行安全性。设为 on 后,任何「本该安全但其实不安全」的函数或代码路径都会暴露出来(抛错或给出异常结果)。官方明确声明它「不应在生产数据库上使用」。

sql
-- 仅用于测试:强制所有安全查询并行,用于发现并行安全 bug
SET debug_parallel_query = on;
EXPLAIN (ANALYZE) SELECT my_plpgsql_func(id) FROM some_table;
RESET debug_parallel_query;

enable_* 开关:允许或禁止某种并行计划形状 ​

这些规划器开关默认全部为 on,可用来定位「某种计划形状是否有问题」:

参数默认值影响的计划形状
enable_parallel_appendon是否允许 Parallel Append
enable_parallel_hashon是否允许 Parallel Hash(共享哈希表)
enable_async_appendonPostgreSQL 18 新增,控制异步 Append
enable_partitionwise_joinoff分区表之间的连接是否「按分区」并行处理
enable_partitionwise_aggregateoff分区表聚合是否按分区分组处理

注意后两项默认是 off——它们属于「分区智能」,与并行相关但独立控制。开启它们可能带来更多并行机会,也可能因计划变复杂而变慢,必须实测。

sql
-- 用开关排除可能性:若关掉 Parallel Append 后有改善,说明该形状不适配当前数据
SET enable_parallel_append = off;
EXPLAIN (ANALYZE) SELECT ... ;
RESET enable_parallel_append;

与并行间接相关的参数 ​

几个不叫「parallel」但直接影响并行效果的参数:

参数默认值与并行的关系
work_mem4MB每个 worker 各自独立分配;并行会成倍放大内存
hash_mem_multiplier2.0哈希类节点可用内存上限倍数,参与并行内存放大计算
effective_io_concurrency16PostgreSQL 18 中并行扫描与 AIO 配合的并发 I/O 数
dynamic_shared_memory_type平台相关Parallel Hash 需要动态共享内存(DSM)建立共享哈希表
max_worker_processes8池子不够时,worker 会「规划了但启动不了」
sql
-- 确认动态共享内存可用(Parallel Hash 依赖它)
SHOW dynamic_shared_memory_type;

-- 并行环境下的内存预算示例
SELECT current_setting('work_mem')               AS work_mem,
       current_setting('hash_mem_multiplier')    AS hash_mult,
       current_setting('max_parallel_workers_per_gather') AS workers_per_gather;

如何观测参数的真实影响 ​

规划了不代表真的启动了。观测并行效果要同时看两处:

sql
-- ① 单条查询:规划 vs 实际
EXPLAIN (ANALYZE, VERBOSE, BUFFERS)
SELECT count(*) FROM fact_orders WHERE status = 'paid';
sql
-- ② 实例级:规划总数 vs 实际启动总数(PG18 新增字段)
SELECT datname,
       parallel_workers_to_launch,
       parallel_workers_launched,
       CASE WHEN parallel_workers_to_launch > 0
            THEN round(100.0 * parallel_workers_launched / parallel_workers_to_launch, 1)
       END AS launch_pct
FROM pg_stat_database
WHERE datname = current_database();

若 launch_pct 长期明显低于 100%,说明参数之间存在矛盾:不是 max_worker_processes 池子太小,就是同时活跃的并行查询太多(max_parallel_workers 被打满)。

实战示例 ​

场景一:worker 数被对象级设置卡住,逐步放开 ​

背景:fact_orders 有 8000 万行(约 6GB),报表查询扫描很慢,观察发现只用了 2 个 worker。

第一步,确认当前状态:

sql
SELECT current_setting('max_worker_processes')              AS pool,
       current_setting('max_parallel_workers')               AS parallel_total,
       current_setting('max_parallel_workers_per_gather')    AS per_gather;
text
 pool | parallel_total | per_gather
------+----------------+------------
 8    | 8              | 2

规划器只用了默认的 2 个 worker,瓶颈在 max_parallel_workers_per_gather。

第二步,检查内存预算(work_mem = 32MB,hash_mem_multiplier = 2.0):

text
把 worker 提到 6 后,单个 Hash 节点的最坏内存:
  2.0 × 32MB × (6 + 1) = 448MB
若同时有 5 条这样的报表查询在跑:448MB × 5 ≈ 2.2GB
——必须在实例内存预算之内。

第三步,用表级选项精准放开(不全局调大):

sql
ALTER TABLE fact_orders SET (parallel_workers = 6);

第四步,验证效果:

sql
EXPLAIN (ANALYZE, VERBOSE, BUFFERS)
SELECT count(*) FROM fact_orders WHERE status = 'paid';
text
 Finalize Aggregate  (cost=1000.00..118450.32 rows=1 width=8) (actual time=412.5..418.2 rows=1 loops=1)
   -- Finalize Aggregate 在 leader 上执行,汇总各 worker 的 Partial Aggregate 结果
   Buffers: shared hit=2100 read=498020
   ->  Gather  (cost=1000.00..118450.11 rows=6 width=8) (actual time=410.1..418.1 rows=6 loops=1)
         -- Gather 汇合点:rows=6 表示 6 个 worker(含 leader 参与时是 6 行 partial 结果)
         Workers Planned: 6
         -- 规划器按表级 parallel_workers=6 规划(此前为 2)
         Workers Launched: 6
         -- 实际全部启动成功,说明 worker 池充足
         Buffers: shared hit=2100 read=498020
         ->  Partial Aggregate  (cost=1000.00..1000.01 rows=1 width=8) (...) (actual time=405.2..405.2 rows=1 loops=6)
               -- Partial Aggregate:每个 worker 各做一遍局部聚合,loops=6 表示被 6 个进程各执行一次
               Buffers: shared hit=2100 read=498020
               ->  Parallel Seq Scan on fact_orders  (cost=0.00..105833.33 rows=20833 width=0) (...)
                     -- Parallel Seq Scan:块区间被 6 个进程分片认领
                     Filter: (status = 'paid'::text)
                     Rows Removed by Filter: 1388888
                     -- 每个 worker 平均丢弃约 139 万行;若不并行这部分 CPU 全压在单核上
                     Buffers: shared hit=2100 read=498020
 Planning Time: 0.412 ms
 Execution Time: 1180.335 ms

结论:Workers Planned 与 Workers Launched 都是 6,实际耗时从约 3200ms 降到 1180ms。这个例子说明「表级设置」比全局调参更精准——只有这张事实表获得 6 个 worker。

场景二:参数自相矛盾导致 worker 启动不了 ​

背景:为了加快所有报表,把全局并行度调到 per_gather = 8,但观测到 Workers Launched 经常小于 Planned。

sql
ALTER SYSTEM SET max_parallel_workers_per_gather = 8;
SELECT pg_reload_conf();

之后某个报表查询的计划:

sql
EXPLAIN (ANALYZE, VERBOSE) SELECT count(*) FROM fact_orders;
text
 Finalize Aggregate  (cost=1000.00..95233.10 rows=1 width=8) (actual time=921.4..928.7 rows=1 loops=1)
   ->  Gather  (cost=1000.00..95232.88 rows=8 width=8) (actual time=918.2..928.6 rows=4 loops=1)
         -- rows=4:只拿到 4 份 partial 结果,说明真实只有 4 个进程干了活
         Workers Planned: 8
         -- 规划器「打算」用 8 个
         Workers Launched: 3
         -- 实际只启动了 3 个!规划 8 个纯属浪费
         ->  Partial Aggregate  (...) (actual time=910.0..910.0 rows=1 loops=3)
               -- loops=3 印证:并行部分只被 3 个 worker 执行
               ->  Parallel Seq Scan on fact_orders  (...) (actual time=2.0..870.5 rows=...)
                     -- 每个 worker 扫描的块区间比预期多得多,负载翻倍
 Planning Time: 0.388 ms
 Execution Time: 2410.559 ms

问题定位:max_parallel_workers 仍是 8,但同期有 4 条类似报表并发执行——每条都想拿 8 个,池子瞬间被先到的几条抢光,后到的只能拿到 3 个。

sql
-- 查看当前有多少并行 worker 在跑
SELECT count(*)                                AS running_workers,
       count(DISTINCT leader_pid)              AS concurrent_parallel_queries
FROM pg_stat_activity
WHERE backend_type = 'parallel worker';
text
 running_workers | concurrent_parallel_queries
-----------------+----------------------------
              21 |                          4

21 > 8,说明池子已经是瓶颈。

处理方案(择一):

sql
-- 方案 A:扩大池子(需重启),给并行与复制分别留量
ALTER SYSTEM SET max_worker_processes = 32;
ALTER SYSTEM SET max_parallel_workers = 24;
-- 重启后生效

-- 方案 B:调低单查询并行度,让并发查询公平分配
ALTER SYSTEM SET max_parallel_workers_per_gather = 4;
SELECT pg_reload_conf();

结论:Workers Planned > Workers Launched 不是 bug,而是资源竞争信号。最坏情况是「按并行成本估算选了这条路,实际却串行执行」,比一开始就选串行计划更慢。

注意事项 ​

  1. 不要把 max_parallel_workers_per_gather 一次调到很大。 它会成倍放大 work_mem 占用,高并发下直接打爆内存。

    sql
    -- 错误!未做内存预算就激进上调
    ALTER SYSTEM SET max_parallel_workers_per_gather = 16;
  2. 不要让 max_parallel_workers 超过 max_worker_processes。 并行 worker 从池子里取,超出部分静默无效。

    sql
    -- 错误!max_worker_processes 仍是默认 8,这里设 24 不会生效
    ALTER SYSTEM SET max_parallel_workers = 24;
  3. 不要把 max_parallel_maintenance_workers 设得比 max_parallel_workers 还大。 维护命令的 worker 同样从总配额里取,设大了只会互相等待。

    sql
    -- 错误!总配额只有 8,这里设 8 已占满,查询将无 worker 可用
    ALTER SYSTEM SET max_parallel_maintenance_workers = 8;
  4. 不要在生产库上使用 debug_parallel_query。 它属于开发者选项,会强制给所有安全查询加 Gather,制造大量无谓并行与错误。

    sql
    -- 错误!生产环境强制并行,性能崩塌且可能触发并行安全 bug
    ALTER SYSTEM SET debug_parallel_query = on;
  5. 不要继续使用 force_parallel_mode。 它在 PostgreSQL 16 起已更名为 debug_parallel_query,PG18 文档中不再有这个正式参数名。旧名是开发者选项的历史遗留,本就不该出现在生产配置里。

    sql
    -- 错误!PG16 起应改用 debug_parallel_query,且它属开发者选项、禁止在生产使用
    SET force_parallel_mode = on;
  6. 不要为了「让计划看起来是并行的」而清零成本参数后就不管。 成本参数是诊断手段,不是修复方案;清零后若仍无并行,说明是并行安全或语义限制。

    sql
    -- 不推荐:长期保留这些设置会让规划器失去成本判断力
    ALTER SYSTEM SET parallel_setup_cost  = 0;
    ALTER SYSTEM SET parallel_tuple_cost  = 0;
  7. 不要给所有表都设 parallel_workers。 参数散落在各张表上会难以维护,且小表设了也无用(仍受 min_parallel_table_scan_size 限制)。

    sql
    -- 不推荐:小维表根本不会并行,设置纯属噪音
    ALTER TABLE dim_currency SET (parallel_workers = 8);
  8. 不要把自定义函数「顺手」标成 PARALLEL SAFE。 有副作用(写表、用序列、改事务状态、改配置)的函数标错会导致报错或结果错误。

    sql
    -- 错误!函数内写表,却标成 SAFE
    CREATE FUNCTION bump() RETURNS int
    LANGUAGE plpgsql PARALLEL SAFE
    AS $$ BEGIN UPDATE counters SET n = n + 1; RETURN 1; END $$;
  9. 不要依赖会话内的 SET 去「持久」改善并行。SET 只影响当前会话,作为调优结论必须落到 ALTER TABLE / ALTER SYSTEM。

    sql
    -- 不推荐:断连即失效,运维同事看不到也复现不了
    SET max_parallel_workers_per_gather = 8;
  10. 不要忘记 max_worker_processes 需要重启。 改完 ALTER SYSTEM 后 pg_settings.pending_restart 为 true,此时任何 EXPLAIN 对比都是无效的。

    sql
    -- 检查是否有等待重启的参数
    SELECT name, setting, pending_restart
    FROM pg_settings
    WHERE pending_restart;
  11. 不要让并行与逻辑复制抢 worker。 单节点上同时跑逻辑复制与大批量并行查询时,池子规划必须给复制留出确定份额。

    text
    不推荐的分配(并行可吃满池子):
      max_worker_processes = 8
      max_parallel_workers = 8   ← 复制无处可去

最佳实践 ​

  1. 按「内存」而不是按「CPU 核数」来定 max_parallel_workers_per_gather。 先算 hash_mem_multiplier × work_mem × (N + 1) × 最大并发数,确保不超过实例可用内存的一半。

    text
    work_mem = 32MB, hash_mem_multiplier = 2.0, N = 4
    单查询最坏 ≈ 2.0 × 32MB × 5 = 320MB
    允许 8 条并发并行查询 → 最多 2.56GB
  2. 保证四层限额单调不增。 让 max_worker_processes ≥ max_parallel_workers ≥ max_parallel_workers_per_gather,并给复制、autovacuum 预留明确份额。

    text
    max_worker_processes             = 24   -- 池子(需重启)
    max_parallel_workers             = 16   -- 并行配额
    max_parallel_workers_per_gather  = 4    -- 单查询上限
    max_parallel_maintenance_workers = 2    -- 维护命令上限
  3. 优先用表级 parallel_workers 做精细控制。 只给少数大型事实表设置,其它表保持自动推断,避免全局调参「殃及池鱼」。

    sql
    ALTER TABLE fact_orders   SET (parallel_workers = 6);
    ALTER TABLE fact_events   SET (parallel_workers = 6);
    ALTER TABLE dim_customers SET (parallel_workers = 0);
  4. 升级到 PG18 后先接上两个新监控字段。 持续观察 parallel_workers_to_launch 与 parallel_workers_launched 的差值,作为「池子是否够用」的长期指标。

    sql
    SELECT datname,
           parallel_workers_to_launch,
           parallel_workers_launched,
           round(100.0 * parallel_workers_launched
                 / NULLIF(parallel_workers_to_launch, 0), 1) AS launch_pct
    FROM pg_stat_database
    WHERE datname IS NOT NULL
    ORDER BY parallel_workers_to_launch DESC;
  5. 审计自定义函数的并行标记。 尤其是从旧版本迁移过来的函数,很多本该 SAFE 的仍停留在默认 UNSAFE。

    sql
    SELECT n.nspname, p.proname, p.proparallel
    FROM pg_proc p
    JOIN pg_namespace n ON n.oid = p.pronamespace
    WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
      AND p.proparallel = 'u'
    ORDER BY 1, 2;
  6. 给并行查询的单节点 work_mem 单独评估,不必全局调大。 更稳妥的做法是保持全局 work_mem 较小,只在报表会话里 SET。

    sql
    -- 报表会话里局部提高
    SET work_mem = '64MB';
    SET max_parallel_workers_per_gather = 4;
  7. 把并行与 OLTP 负载分离。 报表类并行查询放到只读副本,主库上保持保守的并行参数,是最省事的隔离手段。

  8. 调整任何并行参数前,先固定一个对照实验。 同一条 SQL 在改前/改后各跑一次 EXPLAIN (ANALYZE, BUFFERS),记录 Workers Planned、Workers Launched 与实际耗时三项。

  9. 用 pg_stat_activity.leader_pid 监控实时并行度。 它能告诉你此刻有多少并行查询、多少 worker 在跑,是判断「是否已触及天花板」的最直接依据。

    sql
    SELECT count(*)                       AS total_workers,
           count(DISTINCT leader_pid)     AS parallel_queries
    FROM pg_stat_activity
    WHERE leader_pid IS NOT NULL;
  10. 对维护窗口的并行单独规划。 max_parallel_maintenance_workers 调大能显著缩短大表建索引时间,但要安排在低峰期,避免与业务并行查询抢 worker。

    sql
    -- 维护窗口内提高,结束后还原
    ALTER SYSTEM SET max_parallel_maintenance_workers = 4;
    SELECT pg_reload_conf();
  11. 不要用 debug_parallel_query 做性能调优。 它的唯一用途是暴露并行安全 bug;调优应沿着「补统计信息 → 修 SQL → 修函数标记 → 调参数」的顺序进行。

  12. 把最终生效的参数写进变更记录。 并行参数之间互相牵制,只改一个往往无效;记录完整的四层限额与内存预算,才能解释清楚「为什么这么设」。

下一节

继续学习:并行计划的操作与诊断,了解如何阅读各类并行计划的执行细节与排查常见问题。

参考链接 ​