分区裁剪排除的是整张不可能包含匹配行的分区。它与在剩余分区内部利用索引寻找行不同,也不是按页工作的缓存技巧。PostgreSQL 可以在查询规划阶段完成排除,也可以等参数值在执行过程中可用后再裁剪。要判断它是否生效,需要查看实际访问的分区,而不是只看最终返回了多少行。

证明哪些分区不可能命中

声明式分区的边界来自分区键。把日期键限制在十月,可以证明九月和十一月与查询无关。裁剪依赖这些边界,而不是估计其他分区“可能很少匹配”。统计信息仍影响其他规划决策,但行数估计不准,不会让分区归属变成概率判断;这两个层面的信息应分开解释。

裁剪不要求先建索引。排除无关分区后,如果剩余分区的大部分行都需要读取,PostgreSQL 完全可能选择顺序扫描。所以,出现 Index Scan 并不是裁剪成功的判据。应先检查物理关系名和计划结构,再单独评价剩余每张表内部的访问方式。顺序扫描一张相关分区,仍然可能已经省掉其他分区的大量工作。

条件必须能对应分区键及其比较语义。把日期列转换成文本,可能隐藏直接的边界关系,即使某个字符串条件恰好仍选出相同行。应使用有类型的参数和直接表达目标区间的条件,不能假定分区键外面套任意表达式都保留裁剪能力,甚至不能默认它保留原来的日期语义。

参数也能在执行时触发裁剪

常量可以支持规划期裁剪,使计划只留下相关子表。预备语句的通用计划无法提前知道未来每次传入的参数值,但这不表示它必须扫描全部分区:执行初始化时已经知道的值,也可以用于执行期裁剪。看见通用计划中原本存在多个分支,还不足以判断最终都会读取。

EXPLAIN ANALYZE 用 Subplans Removed 报告初始化阶段排除的分区。另一些情况,例如嵌套循环内侧参数在执行期间变化,可以在实际执行过程中继续裁剪。分析这些场景时,需要查看循环次数和子计划是否运行。某个节点显示从未执行,也可能是外层没有输入等其他原因,不能脱离完整计划单独当成裁剪证据。

因此,不能把这些规则简化成“只要不是常量表达式,就会失去裁剪”。参数何时可用,以及表达式是否受支持,共同决定何时能够排除分区。定制计划和通用计划可能不同,但返回同样答案。真正值得确认的是:负载使用真实参数时,哪些分区实际被访问了。

坏掉的样子

只过滤无关字段,可能让所有分区仍然相关;把键转换成无法对应边界的表达式,也可能产生同样结果。调整分区布局前,应检查准确条件和 enable_partition_pruning 设置。手工添加相互矛盾的 CHECK 约束并不是修复方法;普通本地声明式分区会执行自己的边界规则,而基于 CHECK 的约束排除是另一种优化机制。

裁剪也不保证没有锁或规划成本可以忽略。执行初始化阶段排除的分区,在执行开始时仍然会被锁定。大型分区层级可能消耗规划时间和会话内的元数据内存,即使最后只扫描少数子表。应根据真实查询和维护模式选择层级,而不是追求尽可能多的分区数量。

最后,即使裁掉两个分区,第三个分区内部仍可能有几千条无关行。合适的局部索引可能帮助第二阶段。最终返回一百行,不证明只检查了一百行;缓冲命中也不等于物理磁盘读取。必须沿着计划节点看清扫描、过滤与输出之间的区别。

快速验证

把下面内容保存为 partition-pruning.sql,在可丢弃的 PostgreSQL 18 数据库中执行 psql -X -v ON_ERROR_STOP=1 -f partition-pruning.sql。脚本在事务内启用裁剪,并用通用预备计划展示初始化阶段的行为。临时分区上没有建立任何索引。

\set ON_ERROR_STOP on
BEGIN;
SET LOCAL datestyle='ISO, YMD';
SET LOCAL enable_partition_pruning=on;
CREATE TEMP TABLE observations (day date,id integer) PARTITION BY RANGE(day);
CREATE TEMP TABLE obs_sep PARTITION OF observations FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TEMP TABLE obs_oct PARTITION OF observations FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
CREATE TEMP TABLE obs_nov PARTITION OF observations FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');
INSERT INTO observations SELECT DATE '2026-09-01'+g%90,g FROM generate_series(0,8999) AS g;
ANALYZE observations;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT count(*) FROM observations WHERE day>=DATE '2026-10-01' AND day<DATE '2026-11-01';
SET LOCAL plan_cache_mode=force_generic_plan;
PREPARE by_day(date) AS SELECT count(*) FROM observations WHERE day=$1;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) EXECUTE by_day(DATE '2026-10-10');
EXECUTE by_day(DATE '2026-10-10');
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT count(*) FROM observations WHERE day::text='2026-10-10';
DO $$ BEGIN
  IF (SELECT count(*) FROM observations WHERE day=DATE '2026-10-10') <> 100 OR
     (SELECT count(*) FROM observations WHERE day>=DATE '2026-10-01' AND day<DATE '2026-11-01') <> 3100 THEN
    RAISE EXCEPTION 'unexpected pruned result';
  END IF;
END $$;
DEALLOCATE by_day;
ROLLBACK;

PostgreSQL 18.6 把常量十月区间规划为只扫描 obs_oct,读取三千一百条匹配行。通用日期查询报告 Subplans Removed 为二,返回一百行,但仍需在十月分区中过滤三千条其他记录。这同时展示了成功排除无关分区,以及相关分区内部尚未消除的工作量。

文本转换条件扫描三个子表,在明确指定 ISO DateStyle 后,仍返回相同的一百行。断言检查有类型查询的结果数量,DEALLOCATE 和回滚分别清理预备语句与临时表。这个结果验证当前示例中条件与裁剪的关系;选择索引或调整生产分区粒度前,还应测量有代表性的实际计划,不能直接把小实验的结构当成性能收益保证。

来源