并行配置参数(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,要依次穿过四道闸门,取四者中的最小值:
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 的重要变化
有三处与并行配置直接相关的变化,必须知道:
force_parallel_mode已更名为debug_parallel_query。 PostgreSQL 16 起官方把该参数由force_parallel_mode更名为debug_parallel_query(枚举型,取值off/on/regress,默认off),它属于 §19.17 Developer Options,官方明确要求「不应在生产数据库上使用」。PostgreSQL 18 文档中只保留新名,旧名不应再出现在任何配置里。- 异步 I/O(AIO,Asynchronous I/O)落地。 并行顺序扫描、位图堆扫描可以借助 AIO 提升吞吐,相关旋钮是
io_method与effective_io_concurrency(后者默认16)。 - 监控字段补齐。
pg_stat_database新增parallel_workers_to_launch与parallel_workers_launched,可以量化「规划了却没启动」的 worker 比例。
语法与用法
查看当前值
-- 一次性查看所有并行相关参数
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:是否「已改配置但还没重启」。
单参数确认
SHOW max_worker_processes; -- 8(默认)
SHOW max_parallel_workers; -- 8(默认)
SHOW max_parallel_workers_per_gather; -- 2(默认)临时改(会话级)
-- 只影响当前会话,事务结束/断连后自动还原
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;持久改(实例级)
-- 写入 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对象级改(表级)
-- 只影响针对这张表的扫描;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%';函数与聚合的并行标记
-- 函数:默认 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 启动时就要确定(要预留共享内存中的进程槽位)。
-- 检查池子的实际占用情况(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 是刻意保守的。原因是内存放大:
单个并行节点最坏内存占用 ≈ 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——而且是每个并发查询各自这么多。
-- 会话级验证:同一条查询在 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 这个总配额里取。
-- 让大表建索引用上更多 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 本身较忙的场景。
-- 关掉 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_cost | 1000 | 启动并行 worker 的固定代价(进程启动、内存初始化、状态共享等) |
parallel_tuple_cost | 0.1 | 从 worker 向 leader 传递每一行的代价 |
parallel_tuple_cost 是按行计费的,这一点极其关键。如果并行部分产出上千万行,光是「搬回 leader」的代价就有 0.1 × 1e7 = 1e6,足以让规划器放弃并行。这正是「分组数接近输入行数」的聚合不适合并行的原因——Finalize Aggregate 要接收的行数太多。
诊断用法:如果确信并行更快但规划器没选,可以临时把两者清零,看规划器是否只是被成本吓住了。
-- 诊断:清零后看是否会选并行计划
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_size | 8MB | 表需要超过这个大小,才考虑并行顺序扫描 |
min_parallel_index_scan_size | 512kB | 索引需要超过这个大小,才考虑并行索引扫描 |
这两个是下限,不是「只要超过就一定并行」。它们和成本参数配合工作:表够大只是「有资格」,还得成本划算才会被选中。
自动推断的 worker 数也与表大小挂钩:表越大,规划器给的 Workers Planned 越多(在 max_parallel_workers_per_gather 之内)。粗略关系是表大小每翻一倍,并行度可增加一级。
-- 看一张表的物理大小,判断是否够格并行
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,小表不受影响。
-- 大型事实表:多用 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 SAFE | s | 可以安全地在 worker 中执行,不受并行上下文影响 |
PARALLEL RESTRICTED | r | 只能由 leader 执行;出现在并行部分会「截断」并行范围 |
PARALLEL UNSAFE | u | 绝对不能出现在并行计划中 |
用户自定义函数默认是 PARALLEL UNSAFE。 这是最常见的「查询无法并行」原因:函数体内只是简单计算,但因为没标 SAFE,整个查询被降级为串行。
永久属于 PARALLEL RESTRICTED 的典型操作:扫描 CTE、扫描临时表、执行 DECLARE CURSOR。它们不会报错,只会静默地让并行范围缩小。
-- 错误的标记会直接导致并行查询报错
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 后,任何「本该安全但其实不安全」的函数或代码路径都会暴露出来(抛错或给出异常结果)。官方明确声明它「不应在生产数据库上使用」。
-- 仅用于测试:强制所有安全查询并行,用于发现并行安全 bug
SET debug_parallel_query = on;
EXPLAIN (ANALYZE) SELECT my_plpgsql_func(id) FROM some_table;
RESET debug_parallel_query;enable_* 开关:允许或禁止某种并行计划形状
这些规划器开关默认全部为 on,可用来定位「某种计划形状是否有问题」:
| 参数 | 默认值 | 影响的计划形状 |
|---|---|---|
enable_parallel_append | on | 是否允许 Parallel Append |
enable_parallel_hash | on | 是否允许 Parallel Hash(共享哈希表) |
enable_async_append | on | PostgreSQL 18 新增,控制异步 Append |
enable_partitionwise_join | off | 分区表之间的连接是否「按分区」并行处理 |
enable_partitionwise_aggregate | off | 分区表聚合是否按分区分组处理 |
注意后两项默认是 off——它们属于「分区智能」,与并行相关但独立控制。开启它们可能带来更多并行机会,也可能因计划变复杂而变慢,必须实测。
-- 用开关排除可能性:若关掉 Parallel Append 后有改善,说明该形状不适配当前数据
SET enable_parallel_append = off;
EXPLAIN (ANALYZE) SELECT ... ;
RESET enable_parallel_append;与并行间接相关的参数
几个不叫「parallel」但直接影响并行效果的参数:
| 参数 | 默认值 | 与并行的关系 |
|---|---|---|
work_mem | 4MB | 每个 worker 各自独立分配;并行会成倍放大内存 |
hash_mem_multiplier | 2.0 | 哈希类节点可用内存上限倍数,参与并行内存放大计算 |
effective_io_concurrency | 16 | PostgreSQL 18 中并行扫描与 AIO 配合的并发 I/O 数 |
dynamic_shared_memory_type | 平台相关 | Parallel Hash 需要动态共享内存(DSM)建立共享哈希表 |
max_worker_processes | 8 | 池子不够时,worker 会「规划了但启动不了」 |
-- 确认动态共享内存可用(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;如何观测参数的真实影响
规划了不代表真的启动了。观测并行效果要同时看两处:
-- ① 单条查询:规划 vs 实际
EXPLAIN (ANALYZE, VERBOSE, BUFFERS)
SELECT count(*) FROM fact_orders WHERE status = 'paid';-- ② 实例级:规划总数 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。
第一步,确认当前状态:
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; pool | parallel_total | per_gather
------+----------------+------------
8 | 8 | 2规划器只用了默认的 2 个 worker,瓶颈在 max_parallel_workers_per_gather。
第二步,检查内存预算(work_mem = 32MB,hash_mem_multiplier = 2.0):
把 worker 提到 6 后,单个 Hash 节点的最坏内存:
2.0 × 32MB × (6 + 1) = 448MB
若同时有 5 条这样的报表查询在跑:448MB × 5 ≈ 2.2GB
——必须在实例内存预算之内。第三步,用表级选项精准放开(不全局调大):
ALTER TABLE fact_orders SET (parallel_workers = 6);第四步,验证效果:
EXPLAIN (ANALYZE, VERBOSE, BUFFERS)
SELECT count(*) FROM fact_orders WHERE status = 'paid'; 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。
ALTER SYSTEM SET max_parallel_workers_per_gather = 8;
SELECT pg_reload_conf();之后某个报表查询的计划:
EXPLAIN (ANALYZE, VERBOSE) SELECT count(*) FROM fact_orders; 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 个。
-- 查看当前有多少并行 worker 在跑
SELECT count(*) AS running_workers,
count(DISTINCT leader_pid) AS concurrent_parallel_queries
FROM pg_stat_activity
WHERE backend_type = 'parallel worker'; running_workers | concurrent_parallel_queries
-----------------+----------------------------
21 | 421 > 8,说明池子已经是瓶颈。
处理方案(择一):
-- 方案 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,而是资源竞争信号。最坏情况是「按并行成本估算选了这条路,实际却串行执行」,比一开始就选串行计划更慢。
注意事项
不要把
max_parallel_workers_per_gather一次调到很大。 它会成倍放大work_mem占用,高并发下直接打爆内存。sql-- 错误!未做内存预算就激进上调 ALTER SYSTEM SET max_parallel_workers_per_gather = 16;不要让
max_parallel_workers超过max_worker_processes。 并行 worker 从池子里取,超出部分静默无效。sql-- 错误!max_worker_processes 仍是默认 8,这里设 24 不会生效 ALTER SYSTEM SET max_parallel_workers = 24;不要把
max_parallel_maintenance_workers设得比max_parallel_workers还大。 维护命令的 worker 同样从总配额里取,设大了只会互相等待。sql-- 错误!总配额只有 8,这里设 8 已占满,查询将无 worker 可用 ALTER SYSTEM SET max_parallel_maintenance_workers = 8;不要在生产库上使用
debug_parallel_query。 它属于开发者选项,会强制给所有安全查询加Gather,制造大量无谓并行与错误。sql-- 错误!生产环境强制并行,性能崩塌且可能触发并行安全 bug ALTER SYSTEM SET debug_parallel_query = on;不要继续使用
force_parallel_mode。 它在 PostgreSQL 16 起已更名为debug_parallel_query,PG18 文档中不再有这个正式参数名。旧名是开发者选项的历史遗留,本就不该出现在生产配置里。sql-- 错误!PG16 起应改用 debug_parallel_query,且它属开发者选项、禁止在生产使用 SET force_parallel_mode = on;不要为了「让计划看起来是并行的」而清零成本参数后就不管。 成本参数是诊断手段,不是修复方案;清零后若仍无并行,说明是并行安全或语义限制。
sql-- 不推荐:长期保留这些设置会让规划器失去成本判断力 ALTER SYSTEM SET parallel_setup_cost = 0; ALTER SYSTEM SET parallel_tuple_cost = 0;不要给所有表都设
parallel_workers。 参数散落在各张表上会难以维护,且小表设了也无用(仍受min_parallel_table_scan_size限制)。sql-- 不推荐:小维表根本不会并行,设置纯属噪音 ALTER TABLE dim_currency SET (parallel_workers = 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 $$;不要依赖会话内的
SET去「持久」改善并行。SET只影响当前会话,作为调优结论必须落到ALTER TABLE/ALTER SYSTEM。sql-- 不推荐:断连即失效,运维同事看不到也复现不了 SET max_parallel_workers_per_gather = 8;不要忘记
max_worker_processes需要重启。 改完ALTER SYSTEM后pg_settings.pending_restart为true,此时任何EXPLAIN对比都是无效的。sql-- 检查是否有等待重启的参数 SELECT name, setting, pending_restart FROM pg_settings WHERE pending_restart;不要让并行与逻辑复制抢 worker。 单节点上同时跑逻辑复制与大批量并行查询时,池子规划必须给复制留出确定份额。
text不推荐的分配(并行可吃满池子): max_worker_processes = 8 max_parallel_workers = 8 ← 复制无处可去
最佳实践
按「内存」而不是按「CPU 核数」来定
max_parallel_workers_per_gather。 先算hash_mem_multiplier × work_mem × (N + 1) × 最大并发数,确保不超过实例可用内存的一半。textwork_mem = 32MB, hash_mem_multiplier = 2.0, N = 4 单查询最坏 ≈ 2.0 × 32MB × 5 = 320MB 允许 8 条并发并行查询 → 最多 2.56GB保证四层限额单调不增。 让
max_worker_processes ≥ max_parallel_workers ≥ max_parallel_workers_per_gather,并给复制、autovacuum 预留明确份额。textmax_worker_processes = 24 -- 池子(需重启) max_parallel_workers = 16 -- 并行配额 max_parallel_workers_per_gather = 4 -- 单查询上限 max_parallel_maintenance_workers = 2 -- 维护命令上限优先用表级
parallel_workers做精细控制。 只给少数大型事实表设置,其它表保持自动推断,避免全局调参「殃及池鱼」。sqlALTER TABLE fact_orders SET (parallel_workers = 6); ALTER TABLE fact_events SET (parallel_workers = 6); ALTER TABLE dim_customers SET (parallel_workers = 0);升级到 PG18 后先接上两个新监控字段。 持续观察
parallel_workers_to_launch与parallel_workers_launched的差值,作为「池子是否够用」的长期指标。sqlSELECT 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;审计自定义函数的并行标记。 尤其是从旧版本迁移过来的函数,很多本该
SAFE的仍停留在默认UNSAFE。sqlSELECT 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;给并行查询的单节点
work_mem单独评估,不必全局调大。 更稳妥的做法是保持全局work_mem较小,只在报表会话里SET。sql-- 报表会话里局部提高 SET work_mem = '64MB'; SET max_parallel_workers_per_gather = 4;把并行与 OLTP 负载分离。 报表类并行查询放到只读副本,主库上保持保守的并行参数,是最省事的隔离手段。
调整任何并行参数前,先固定一个对照实验。 同一条 SQL 在改前/改后各跑一次
EXPLAIN (ANALYZE, BUFFERS),记录Workers Planned、Workers Launched与实际耗时三项。用
pg_stat_activity.leader_pid监控实时并行度。 它能告诉你此刻有多少并行查询、多少 worker 在跑,是判断「是否已触及天花板」的最直接依据。sqlSELECT count(*) AS total_workers, count(DISTINCT leader_pid) AS parallel_queries FROM pg_stat_activity WHERE leader_pid IS NOT NULL;对维护窗口的并行单独规划。
max_parallel_maintenance_workers调大能显著缩短大表建索引时间,但要安排在低峰期,避免与业务并行查询抢 worker。sql-- 维护窗口内提高,结束后还原 ALTER SYSTEM SET max_parallel_maintenance_workers = 4; SELECT pg_reload_conf();不要用
debug_parallel_query做性能调优。 它的唯一用途是暴露并行安全 bug;调优应沿着「补统计信息 → 修 SQL → 修函数标记 → 调参数」的顺序进行。把最终生效的参数写进变更记录。 并行参数之间互相牵制,只改一个往往无效;记录完整的四层限额与内存预算,才能解释清楚「为什么这么设」。
下一节
继续学习:并行计划的操作与诊断,了解如何阅读各类并行计划的执行细节与排查常见问题。