enable_parallel_append 是 PostgreSQL 查询规划器里的一个 GUC 开关。它影响的是 Parallel Append 计划节点:当查询需要读取多个分区表、继承子表,或者执行 UNION ALL 的多个分支时,数据库可以让多个 worker 同时处理不同分支,而不是像普通 Append 那样一个接一个扫完。
这件事听起来很小,但对分区表和报表查询很实际:如果你的数据天然分散在多个分区里,顺序扫分区会把并行度浪费掉;Parallel Append 则尝试把 worker 铺到多个分支上,让 CPU、I/O 和缓存利用更接近“多车道”。
它改变的不是语义,而是执行形状
enable_parallel_append 不会改变查询结果,也不会让 PostgreSQL 忽略事务隔离或排序语义。它只是允许规划器在合适的时候选择 Parallel Append。
典型场景包括:
- 分区表:按时间、租户、地域等拆成多个 partition。
UNION ALL:多个子查询结果简单拼接。- 继承表:老式分表或兼容性结构。
普通 Append 更像这样:
scan partition_2024_01
scan partition_2024_02
scan partition_2024_03
...
Parallel Append 的核心差别是:worker 可以分头处理不同分区或不同 UNION ALL 分支。
worker 1 -> scan partition_2024_01
worker 2 -> scan partition_2024_02
worker 3 -> scan partition_2024_03
如果每个分区都足够大,这种执行形状通常更容易把机器资源吃满。反过来,如果分区很小、数量很多,调度开销也可能抵消收益。
用一个可跑的例子观察计划变化
下面的示例可以在本地 PostgreSQL 里改造运行。它创建一个按日期分区的订单表,插入一些测试数据,然后对跨分区查询执行 EXPLAIN。
运行前请确认:
- PostgreSQL 版本支持并行查询和分区表。
- 测试库允许创建表。
- 如果数据量太小,规划器可能不会选择并行计划;示例里会适度调低并行成本,方便观察。
-- 在 psql 中执行
DROP TABLE IF EXISTS orders_demo CASCADE;
CREATE TABLE orders_demo (
id bigserial,
created_at date NOT NULL,
customer_id int NOT NULL,
amount numeric(12, 2) NOT NULL
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_demo_2024_01 PARTITION OF orders_demo
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_demo_2024_02 PARTITION OF orders_demo
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
CREATE TABLE orders_demo_2024_03 PARTITION OF orders_demo
FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
INSERT INTO orders_demo (created_at, customer_id, amount)
SELECT
date '2024-01-01' + (g % 90),
(random() * 10000)::int,
round((random() * 500)::numeric, 2)
FROM generate_series(1, 900000) AS g;
ANALYZE orders_demo;
-- 为了更容易在小测试环境看到并行计划,调低成本参数。
-- 生产环境不要照抄,应该通过压测决定。
SET max_parallel_workers_per_gather = 4;
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;
SET min_parallel_table_scan_size = 0;
SET min_parallel_index_scan_size = 0;
SET enable_parallel_append = on;
EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS)
SELECT date_trunc('month', created_at)::date AS month,
count(*) AS order_count,
sum(amount) AS total_amount
FROM orders_demo
WHERE created_at >= date '2024-01-01'
AND created_at < date '2024-04-01'
GROUP BY 1
ORDER BY 1;
你要在输出里找的关键词是:
Parallel Append
如果改成下面这样,再执行同一个查询:
SET enable_parallel_append = off;
EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS)
SELECT date_trunc('month', created_at)::date AS month,
count(*) AS order_count,
sum(amount) AS total_amount
FROM orders_demo
WHERE created_at >= date '2024-01-01'
AND created_at < date '2024-04-01'
GROUP BY 1
ORDER BY 1;
你可能会看到规划器选择不同的 Append 形态,或者并行度下降。实际结果取决于 PostgreSQL 版本、表大小、统计信息、硬件、其他 GUC,以及查询本身。
UNION ALL 也会受益
Parallel Append 不只属于分区表。多个可以独立执行的 UNION ALL 分支,也可能成为它的目标。
可以这样实践:把几个结构相同、互不依赖的聚合分支拼起来,让规划器有机会并行处理。
SET max_parallel_workers_per_gather = 4;
SET enable_parallel_append = on;
EXPLAIN (ANALYZE, COSTS, VERBOSE)
SELECT 'high_value' AS bucket, count(*)
FROM orders_demo
WHERE amount >= 400
UNION ALL
SELECT 'mid_value' AS bucket, count(*)
FROM orders_demo
WHERE amount >= 100 AND amount < 400
UNION ALL
SELECT 'low_value' AS bucket, count(*)
FROM orders_demo
WHERE amount < 100;
这类查询的关键点是:分支之间没有去重、排序或互相依赖。UNION 需要去重,通常会引入额外步骤;UNION ALL 只是拼接结果,更适合被 Append 类节点处理。
什么时候应该关注这个开关
不要把 enable_parallel_append 当成“永远更快”的按钮。它更适合拿来解释和验证计划,而不是盲目全局切换。
值得检查它的场景:
- 跨多个分区的大范围扫描很慢。
EXPLAIN里出现普通Append,但机器还有空闲 CPU。- 报表查询由多个
UNION ALL分支组成。 - 分区大小比较均衡,每个分支都有足够工作量。
- 查询没有被低效过滤条件、缺失统计信息或错误索引设计拖垮。
可能收益有限的场景:
- 只命中一个分区。
- 每个分区都很小,worker 调度成本比扫描还高。
- 查询瓶颈在排序、聚合、锁等待或网络传输。
max_parallel_workers_per_gather、表级 parallel 设置或成本参数限制了并行计划。
采用建议:先看计划,再谈开关
实践上,我会把 enable_parallel_append 放在诊断工具箱里,而不是上线手册第一行。
一套稳妥的检查清单是:
- 用
EXPLAIN (ANALYZE, BUFFERS)记录当前计划。 - 在会话级执行
SET enable_parallel_append = off/on做对照。 - 观察是否出现
Parallel Append、worker 数量、每个分支耗时和 buffer 读取。 - 确认统计信息新鲜:必要时执行
ANALYZE。 - 只在压测证明收益稳定后,再考虑调整数据库、角色或应用会话级配置。
Parallel Append 的价值在于让 PostgreSQL 不必排队扫完一个个分区。对分区表和 UNION ALL 查询来说,它给规划器多了一种更贴近硬件并行能力的执行方式。真正的判断标准仍然是计划和耗时:看它有没有出现,看 worker 有没有干上活,看总耗时有没有稳定下降。