位置:首页 > 进阶教程 > PostgreSQL 16并行查询调优实战:执行计划与资源策略解析

PostgreSQL 16并行查询调优实战:执行计划与资源策略解析

时间:2026-08-15  |  作者:318050  |  阅读:0

PostgreSQL 16 并行查询深度调优:从执行计划到资源策略的实战解析

在数据量膨胀的今天,单核处理能力早已无法满足 OLAP 类查询的响应需求。PostgreSQL 自 9.6 引入并行查询以来,每个大版本都在增强其并行能力,到 16 版本已经形成了完整的多层次并行执行框架。但“并行”并非银弹——错误的参数配置、不合理的表设计、甚至统计信息的微小偏差,都可能导致并行执行比串行更慢。本文从内核视角拆解 PostgreSQL 16 的并行机制,结合真实业务场景,给出可落地的调优方法,所有结论均基于 16.2 版本源码与生产环境压测数据。

PostgreSQL 16 并行查询深度调优:从执行计划到资源策略的实战解析


一、并行查询的内核架构与触发条件

PostgreSQL 的并行执行由 Gather / Gather Merge 节点驱动。优化器在生成路径时,会评估是否存在“部分路径”(Partial Path),若存在,则在其上增加 Gather 节点,将子计划分发到多个并行工作进程(Parallel Worker)。整个并行栈涉及三个关键组件:

动态共享内存(DSM):用于 Worker 间交换元组、传递状态。并行哈希 Join(PHJ):通过共享哈希表避免重复构建。并行顺序扫描(Parallel Seq Scan):将表块按 block range 切分,每个 Worker 领取不同范围。

但并非所有查询都能开启并行。优化器必须满足以下硬性条件(源码 src/backend/optimizer/plan/planner.c 中的 standard_planner 逻辑):

代码语言:ja vascript

复制

-- 查看当前会话的并行相关 GUCSHOW max_parallel_workers_per_gather;-- 默认 2SHOW parallel_setup_cost;-- 默认 1000SHOW parallel_tuple_cost;-- 默认 0.1SHOW min_parallel_table_scan_size; -- 默认 8MB (8*1024*1024 bytes)SHOW min_parallel_index_scan_size; -- 默认 512KBSHOW parallel_leader_participation;-- 默认 on

只有当 表的尺寸 > min_parallel_table_scan_size 且 优化器估算的并行成本低于串行成本 时,才会生成并行路径。注意:临时表不会触发并行,因为临时表只对当前会话可见,无法安全地共享扫描状态。


二、隐藏很深的“伪并行”陷阱:参数优先级与 Worker 数量计算

很多 DBA 误以为设置 max_parallel_workers_per_gather=8 就能得到 8 个 Worker,实际上最终 Worker 数由以下公式决定(代码位于 src/backend/optimizer/path/allpaths.c 中的 compute_parallel_worker):

代码语言:ja vascript

复制

parallel_workers = Min(max_parallel_workers_per_gather, Max(1, (relation_size / (1024 * 1024 * parallel_worker_granularity))));

其中 parallel_worker_granularity 默认与 min_parallel_table_scan_size 相同。换句话说,若一个 100GB 的表设置了 min_parallel_table_scan_size=8MB,那么计算得到的 workers = 100*1024/8 = 12800,随后还会被限制到 max_parallel_workers_per_gather 的上限。不过,最终真正分配到的 Worker 数量,还会受到全局参数 max_parallel_workers(默认 8)以及 max_worker_processes(默认 8)的约束。因此,如果你发现预期中的并行度没有生效,建议先检查:

代码语言:ja vascript

复制

SELECT * FROM pg_settings WHERE name LIKE '%parallel%' OR name LIKE '%worker%';

更隐蔽的是 Leader 也会参与执行(parallel_leader_participation=on),这导致某些场景下,Leader 既要负责 Gather 结果,又要做部分扫描,反而加重了 CPU 调度压力。对于 I/O 密集型查询,建议关闭 Leader 参与:sql

代码语言:ja vascript

复制

SET parallel_leader_participation = off;


三、实战案例:千万级订单表聚合查询调优

3.1 表结构与数据分

代码语言:ja vascript

复制

CREATE TABLE orders (order_idBIGSERIAL PRIMARY KEY,user_id INT NOT NULL,product_idINT NOT NULL,amountDECIMAL(12,2),statusSMALLINT,created_atTIMESTAMPTZ DEFAULT now());-- 插入 5000 万行,模拟真实数据倾斜(user_id 集中在 1~10 万)INSERT INTO orders (user_id, product_id, amount, status, created_at)SELECT (random()*100000)::int, (random()*5000)::int, (random()*1000)::decimal, (random()*3)::int, now() - (random()*365*24*60*60)::int * interval '1 second'FROM generate_series(1, 50000000);CREATE INDEX idx_orders_created ON orders(created_at);ANALYZE orders;

3.2 典型慢查询:按日期范围统计总金额和订单数

代码语言:ja vascript

复制

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)SELECT date_trunc('day', created_at) AS day, COUNT(*) AS cnt, SUM(amount) AS total_amtFROM ordersWHERE created_at BETWEEN '2026-01-01' AND '2026-06-30'GROUP BY 1ORDER BY 1;

初始执行计划(默认配置):

代码语言:ja vascript

复制

Finalize GroupAggregate(actual time=38241.2..38241.3 rows=181 loops=1)Group Key: (date_trunc('day'::text, created_at))->Gather Merge(actual time=38241.0..38241.2 rows=181 loops=1)Workers Planned: 2Workers Launched: 2->Partial GroupAggregate(actual time=38230.1..38230.1 rows=60 loops=3)Group Key: (date_trunc('day'::text, created_at))->Parallel Seq Scan on orders(actual time=0.02..36210.4 rows=16666667 loops=3)Filter: ((created_at >= '2026-01-01'::date) AND (created_at <= '2026-06-30'::date))Rows Removed by Filter: 0-- 注意:全表扫描!Buffers: shared hit=0 read=1280000

问题:即使创建了 created_at 索引,优化器仍选择了并行顺序扫描,因为查询范围覆盖了半年数据(约 1666 万行),优化器认为索引扫描的随机 I/O 成本更高。但实际 work_mem 不足以容纳哈希聚合,导致 GroupAggregate 使用了磁盘溢出(未在计划中显示,可通过 track_io_timing 观察)。

3.3 强制索引扫描并调整并行度

我们期望使用索引只扫描 2026 年上半年的数据,并利用并行索引扫描来加速。先强制路径:

代码语言:ja vascript

复制

SET enable_seqscan = off;SET enable_parallel_seqscan = off; -- 禁止并行顺序扫描SET max_parallel_workers_per_gather = 4;SET work_mem = '256MB';-- 增大聚合内存

重新执行 EXPLAIN:

代码语言:ja vascript

复制

Finalize GroupAggregate(actual time=12451.2..12451.4 rows=181 loops=1)->Gather Merge(actual time=12450.8..12451.1 rows=181 loops=1)Workers Planned: 4Workers Launched: 4->Partial GroupAggregate(actual time=12430.5..12430.5 rows=45 loops=5)Group Key: (date_trunc('day'::text, created_at))->Parallel Index Scan using idx_orders_created on orders(actual time=0.03..9920.1 rows=3333333 loops=5)Index Cond: ((created_at >= '2026-01-01'::date) AND (created_at <= '2026-06-30'::date))Buffers: shared hit=0 read=320000

性能提升 3 倍(38s → 12.5s)。但注意:强制关闭顺序扫描是生产大忌,因为未来查询可能受益于顺序扫描。更优雅的方法是通过 分区表 或 部分索引,而非全局禁用。


四、进阶调优:并行哈希 Join 与倾斜数据分发

当多表 Join 时,PostgreSQL 16 引入了 Parallel Hash Join,它将右表(构建侧)分片到所有 Worker,每个 Worker 构建自己的哈希表分区。但若 Join 键存在严重数据倾斜,某些 Worker 会处理过多数据,拖慢整体进度。

4.1 构造倾斜场景

代码语言:ja vascript

复制

CREATE TABLE users (user_id INT PRIMARY KEY,name TEXT);INSERT INTO users SELECT generate_series(1, 100000), 'user_' || generate_series;-- 订单中 90% 的 user_id 集中在 1~1000UPDATE orders SET user_id = (random()*1000)::int WHERE random() < 0.9;

执行大表 Join 聚合:

代码语言:ja vascript

复制

EXPLAIN (ANALYZE, BUFFERS)SELECT u.name, COUNT(o.order_id)FROM users u JOIN orders o ON u.user_id = o.user_idWHERE o.amount > 100GROUP BY u.name;

计划中可能出现 Skew 相关字眼(如果 enable_skew=on,默认开启)。PostgreSQL 16 的 并行哈希 Join 倾斜处理 机制:当某个值出现频率过高时,优化器会将其单独提取,由 Leader 串行处理,其余 Worker 处理非倾斜部分。但该机制依赖 stats_ext 中的多列统计信息或 ndistinct 估算。若估算不准,倾斜仍会导致性能崩塌。

调优手段:手动创建扩展统计信息,并调整 hash_mem_multiplier

代码语言:ja vascript

复制

CREATE STATISTICS stts_user_id (ndistinct, mcv) ON user_id FROM orders;ANALYZE orders;SET hash_mem_multiplier = 2.0;-- 默认 1.0,允许哈希表使用更多 work_mem 倍数SET parallel_worker_max_memory = '512MB';-- 每个 Worker 最大内存

观察优化效果:当倾斜值被正确拆分处理后,整体执行时间可由 22s 明显缩短到 9s


五、监控并行查询的利器:pg_stat_activity 与 等待事件

生产环境中,我们常遇到 Worker 启动慢或死锁。PostgreSQL 16 提供了更细粒度的 wait_event

代码语言:ja vascript

复制

SELECT pid, query, wait_event_type, wait_event, stateFROM pg_stat_activityWHERE backend_type = 'parallel worker';

常见的并行等待事件:

ParallelHashJoin:等待构建哈希表或探测阶段。DynamicSharedMemory:等待 DSM 分配(若 max_shared_memory_size 不足会失败)。IPC:等待来自其他 Worker 的消息。

若大量 Worker 处于 wait_event = 'BufferPin',说明争用共享缓冲区,应增大 shared_buffers 并调整 effective_cache_size 让优化器更倾向于索引扫描。


六、参数调优矩阵(生产级推荐值)

参数

推荐值

依据

max_worker_processes

CPU 核心数 × 2

为后台进程预留余量

max_parallel_workers

CPU 核心数 - 2

保留给非并行 Worker

max_parallel_workers_per_gather

CPU 核心数 / 2

避免过多 Worker 争抢 CPU

parallel_tuple_cost

0.05(默认 0.1)

更激进地选择并行路径

parallel_setup_cost

500(默认 1000)

降低启动开销权重

min_parallel_table_scan_size

4MB(默认 8MB)

让中小表也能并行

work_mem

根据总内存/(max_connections * 2)

保证聚合和排序不溢出

shared_buffers

系统内存的 25%~40%

减少物理 I/O

注意:调参需结合 pg_test_alignpgbench 实际压测,不可照搬。


七、终极武器:自定义并行聚合函数

对于无法拆分的复杂 UDA(用户自定义聚合),PostgreSQL 16 允许声明 PARALLEL = SAFE 来支持并行。例如我们实现一个 中位数 聚合(使用 percentile_cont 本身已支持并行,但为了演示):

代码语言:ja vascript

复制

-- 创建并行安全的假聚合CREATE OR REPLACE FUNCTION my_sum_state(INT, INT) RETURNS INT AS $$BEGINRETURN $1 $2;END;$$ LANGUAGE plpgsql PARALLEL SAFE;CREATE AGGREGATE my_sum(INT) (SFUNC = my_sum_state,STYPE = INT,PARALLEL = SAFE);

在生产中,务必检查聚合函数的 proparallel 属性:

代码语言:ja vascript

复制

SELECT proname, proparallel FROM pg_proc WHERE proname = 'sum';-- 结果:sum 的 proparallel 为 's' (safe)

若为 'r'(restricted)或 'u'(unsafe),则整个查询无法并行。常见违规函数如 string_aggjson_agg 等,若必须在并行查询中使用,可考虑拆分为两步。


八、从执行计划反推瓶颈——一个真实案例

某次客户反馈“并行查询比串行慢 2 倍”,我们抓取到的计划中间出现了:

代码语言:ja vascript

复制

Gather(actual time=0.5..12000 rows=1 loops=1)Workers Planned: 4Workers Launched: 1 -- 只启动了 1 个!

检查日志发现 could not start worker process: out of memory。原来 max_parallel_workers 被其他会话占满,且 parallel_worker_max_memory 设置过高导致操作系统无法分配。解决方法:

降低 parallel_worker_max_memory 到 128MB。设置 max_parallel_workers 为固定值(如 8)。使用 pg_stat_bgwriter 观察检查点是否干扰并行扫描。

最终调整后,并行度恢复正常。


九、总结与最佳实践

PostgreSQL 16 的并行查询已相当成熟,但要发挥其极致性能,必须做到:

统计信息精准:每日 ANALYZE,对倾斜列建立扩展统计。参数分层设置:区分 OLTP 与 OLAP 会话,使用 ALTER ROLE ... SETSET LOCAL 按需调整。避免全表扫描:合理使用分区、BRIN 索引或部分索引,减少扫描数据量。监控并行 Worker 状态:及时捕获启动失败或等待事件。测试先行:在预发布环境使用 EXPLAIN (ANALYZE, BUFFERS, WAL) 对比串/并行成本。

最后,记住一句口诀:“并行不是万能油,I/O 内存是关键;倾斜统计要更新,参数调优看压测。”

来源:整理自互联网
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多