让 PostgreSQL 并行扫分区:读懂 enable_parallel_append

2026-07-04 30 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:8 分钟

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 放在诊断工具箱里,而不是上线手册第一行。

一套稳妥的检查清单是:

  1. EXPLAIN (ANALYZE, BUFFERS) 记录当前计划。
  2. 在会话级执行 SET enable_parallel_append = off/on 做对照。
  3. 观察是否出现 Parallel Append、worker 数量、每个分支耗时和 buffer 读取。
  4. 确认统计信息新鲜:必要时执行 ANALYZE
  5. 只在压测证明收益稳定后,再考虑调整数据库、角色或应用会话级配置。

Parallel Append 的价值在于让 PostgreSQL 不必排队扫完一个个分区。对分区表和 UNION ALL 查询来说,它给规划器多了一种更贴近硬件并行能力的执行方式。真正的判断标准仍然是计划和耗时:看它有没有出现,看 worker 有没有干上活,看总耗时有没有稳定下降。


相关推荐