关于存储过程并行查询,有一个常见的误解:很多人以为只要把 SQL 放进 CREATE OR REPLACE FUNCTION 里,就能自动享受并行加速。其实不然——并行与否,完全取决于执行语句本身的查询计划,跟函数封装没有直接关系。换句话说,函数只是个壳,里面跑什么计划,优化器说了算。

为什么存储过程里写 SELECT 就不自动并行?
PostgreSQL 的并行能力作用在「查询计划层级」,而不是「函数封装层级」。哪怕你在 plpgsql 函数里写了一个带 GROUP BY 的大查询,只要优化器没选中并行路径,它就还是老老实实串行跑。
怎么判断呢?最直接的办法是看 EXPLAIN ANALYZE 的输出。如果看到的是 GroupAggregate 节点,没有 Gather 或 Partial Aggregate,那就说明并行根本没启动,跟是不是在函数里无关。
常见原因有几个:
- 函数调用本身会引入额外开销(比如变量解析、控制流跳转),这会让优化器更倾向于保守的串行计划。
RETURN QUERY或FOR ... IN SELECT中的查询,仍然按普通查询规则做代价估算,不会特殊照顾。- 如果函数里用了
PERFORM或中间赋值(比如SELECT ... INTO),还可能触发 planner 的 early-exit 逻辑,跳过并行候选路径。
哪些 GROUP BY 查询在函数里才可能并行?
只有满足「可分片 + 可合并」条件的聚合,在函数内执行时才有机会走并行。关键看聚合函数和写法:
string_agg(col, ',')—— 必须不带ORDER BY;带了就退化为串行。array_agg(col)、jsonb_agg(col)—— 同样禁止ORDER BY子句。sum(col)、count(*)、max(col)—— 支持并行;但count(distinct col)不支持。- 所有聚合字段不能含 volatile 函数,比如
string_agg(now()::text, ',')会直接禁用并行。
怎么让函数里的查询真正触发 Gather + Partial Aggregate?
想要让函数里的查询走并行,需要手动干预参数,并且只对当前 session 或当前函数生效。推荐用 SET LOCAL:
- 设 worker 数:
SET LOCAL max_parallel_workers_per_gather = 4;(别超 CPU 核心数) - 压低门槛:
SET LOCAL min_parallel_table_scan_size = 1MB;(小表测试可用,生产建议按实际大小设) - 调低成本:
SET LOCAL parallel_setup_cost = 2;和SET LOCAL parallel_tuple_cost = 0.01; - 确保
work_mem足够:SET LOCAL work_mem = '64MB';(总内存 ≈ (1 + workers) × work_mem)
注意:SET LOCAL 只影响当前函数调用内的 SQL,退出函数即恢复;但如果函数里开了事务块(BEGIN ... END),这些设置仍有效。
最容易被忽略的点:函数体里不能有隐式禁止并行的操作
就算参数全调对了,某些写法也会让整个查询退回到串行:
- 在 SELECT 中调用
random()、now()、clock_timestamp()等 volatile 函数。 - 用了
FOR UPDATE或SELECT ... INTO+ 锁定行(即使没显式写 FOR UPDATE,某些隔离级别下也隐含)。 - 聚合字段上套了表达式,比如
string_agg(lower(name), ',')—— lower 是 stable,但若 name 列含 NULL,部分版本会绕过 partial path。 - 函数声明为
VOLATILE(默认行为),而内部查询又依赖 session 设置 —— 建议显式声明为STABLE,避免 planner 过度保守。
真正起效的信号只有一个:EXPLAIN (ANALYZE, VERBOSE) 输出里出现 Gather 节点,且其子节点明确标着 Partial Aggregate。其他都是假象。
