PostgreSQL 的 enable_partition_pruning 看起来像一个普通优化器开关,但它影响的是分区表查询里非常关键的一步:把明显不需要访问的分区从扫描计划中剔除。容易踩坑的地方在于,分区裁剪不只发生在生成执行计划时,也可能发生在执行过程中。排查慢查询时,只看一眼 EXPLAIN 里的静态计划,往往不够。
分区裁剪到底省了什么
分区表的代价来自“可能要看很多张子表”。如果查询条件能明确落在某个分区范围内,数据库就没有必要扫描其他分区。
例如按月分区的订单表:
SELECT *
FROM orders
WHERE order_date >= DATE '2025-01-01'
AND order_date < DATE '2025-02-01';
如果 orders 按 order_date 做范围分区,理想情况下 PostgreSQL 只访问 2025 年 1 月对应的分区,而不是把全年甚至多年的分区都扫一遍。
enable_partition_pruning 控制的就是这类优化是否启用。通常它应该保持开启,因为关闭它会让 PostgreSQL 更倾向于保留不必要的分区扫描,查询计划会变重。
两个阶段:计划时裁剪和执行时裁剪
这篇内容里最值得记住的一点是:PostgreSQL 的 partition pruning 有两个阶段。
计划时裁剪发生在生成查询计划时。条件里如果有常量,优化器可以直接判断哪些分区不可能匹配,然后把它们排除掉。
EXPLAIN
SELECT *
FROM orders
WHERE order_date = DATE '2025-01-15';
这里的日期是常量,优化器在计划阶段就有机会知道只需要访问哪个分区。
执行时裁剪发生在查询真正运行时。典型场景是查询条件依赖参数、子查询结果或运行期才能确定的值。计划阶段 PostgreSQL 还不知道具体值,因此计划里可能看起来保留了多个分区;到了执行阶段,它再根据真实参数裁掉不需要的分区。
这也是为什么排查时要看 EXPLAIN (ANALYZE),而不只是 EXPLAIN。前者能告诉你执行过程中实际发生了什么。
可以这样实践:用 psql 观察裁剪效果
下面是一段可以直接在测试库里运行的 SQL。它创建一个按日期范围分区的表,然后分别观察开启和关闭 enable_partition_pruning 时的计划差异。
运行前请确认你连接的是测试数据库,因为脚本会删除并重建 demo_orders 表。
DROP TABLE IF EXISTS demo_orders CASCADE;
CREATE TABLE demo_orders (
id bigint GENERATED ALWAYS AS IDENTITY,
order_date date NOT NULL,
amount numeric(12, 2) NOT NULL
) PARTITION BY RANGE (order_date);
CREATE TABLE demo_orders_2025_01 PARTITION OF demo_orders
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE demo_orders_2025_02 PARTITION OF demo_orders
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
CREATE TABLE demo_orders_2025_03 PARTITION OF demo_orders
FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');
INSERT INTO demo_orders (order_date, amount)
SELECT d::date, (random() * 1000)::numeric(12, 2)
FROM generate_series('2025-01-01'::date, '2025-03-31'::date, interval '1 day') AS d;
ANALYZE demo_orders;
SET enable_partition_pruning = on;
EXPLAIN (ANALYZE, COSTS OFF)
SELECT *
FROM demo_orders
WHERE order_date >= DATE '2025-02-10'
AND order_date < DATE '2025-02-11';
SET enable_partition_pruning = off;
EXPLAIN (ANALYZE, COSTS OFF)
SELECT *
FROM demo_orders
WHERE order_date >= DATE '2025-02-10'
AND order_date < DATE '2025-02-11';
RESET enable_partition_pruning;
你要看的不是某个固定的输出文本,而是扫描节点里出现了多少个分区。开启裁剪时,查询应该只需要访问 2 月分区;关闭后,计划通常会保留更多分区扫描。
如果你想从命令行快速跑一次,可以这样:
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f partition_pruning_demo.sql
把 DATABASE_URL 换成你的测试库连接串,或者直接使用 psql -d your_db_name。
排查慢查询时别只盯着一个开关
enable_partition_pruning 是入口,但不是全部。实践里更常见的问题是:查询条件写法让优化器难以判断分区边界。
比较稳妥的写法是让分区键直接出现在谓词里:
-- 更容易被分区裁剪利用
SELECT *
FROM demo_orders
WHERE order_date >= DATE '2025-02-01'
AND order_date < DATE '2025-03-01';
需要谨慎的是把函数套在分区键上:
-- 可以这样作为反例检查:这种写法可能让优化器更难利用分区边界
SELECT *
FROM demo_orders
WHERE date_trunc('month', order_date) = TIMESTAMP '2025-02-01';
这并不是说所有函数表达式都会彻底破坏优化,而是说排查分区裁剪时,应该优先确认谓词是否能清楚表达分区范围。生产 SQL 里越多隐式类型转换、函数包装、复杂表达式,越需要用 EXPLAIN (ANALYZE) 验证真实行为。
采用建议:把验证放进发布流程
对分区表做性能优化时,可以按这个清单走:
- 确认
enable_partition_pruning没被会话、角色或数据库级配置意外关闭。 - 对关键查询同时看
EXPLAIN和EXPLAIN (ANALYZE),区分计划时裁剪和执行时裁剪。 - 谓词尽量直接使用分区键,避免不必要的函数包装和类型绕路。
- 参数化 SQL、预编译语句、ORM 生成 SQL 都要实际解释计划,不要只看手写 SQL。
- 分区数量很多时,把“扫描了哪些分区”作为性能回归检查的一部分。
enable_partition_pruning 的价值不在于让你手动调一个神秘参数,而在于提醒你:分区表的性能要靠可验证的裁剪行为支撑。看清楚计划阶段和执行阶段分别裁掉了什么,才知道查询是在利用分区,还是只是把一张大表拆成了很多小麻烦。