2. PostgreSQL分区机制全景

PostgreSQL 的分区能力并不是一次性设计出来的,而是经历了从“继承 + 约束 + 触发器”的手工方案,到 10 版本引入声明式分区、再到 11~14 版本逐步补齐索引、默认分区、哈希分区、并行裁剪等能力的演进过程。理解这条演进线,是判断“某个项目该用哪种分区、能不能升级、升级后哪些行为会变”的前提。

一、两种分区机制的本质差异

继承式分区(inheritance-based partitioning)是 PostgreSQL 早期唯一可行的方案。它的做法是:先建一张父表,再建若干子表并 INHERITS 父表,然后为每张子表添加 CHECK 约束来限定取值范围,最后通过触发器或应用层逻辑把数据路由到正确的子表。父表本身通常不存数据,查询时依赖 constraint_exclusion 参数来跳过不满足条件的子表。

声明式分区(declarative partitioning)从 PostgreSQL 10 开始提供,语法上直接使用 PARTITION BY RANGE / LIST / HASH,由内核负责路由、裁剪和元数据管理。它把“分区键、分区边界、分区与父表的关系”变成系统目录中的一等公民,而不是靠用户自己维护的约束和触发器。

两者的关键差异可以归纳为以下几点:

  • 路由方式:继承式依赖触发器函数或应用层判断;声明式由内核在 INSERT 时自动路由,无需额外代码。
  • 裁剪依据:继承式依赖子表上的 CHECK 约束与 constraint_exclusion;声明式直接使用分区边界元数据,裁剪更可靠。
  • 索引与约束:继承式父表上的索引不会自动覆盖子表,需要逐表创建;声明式在父表上建索引会级联到所有分区(11 版本起支持)。
  • 默认分区:声明式支持 DEFAULT 分区兜底;继承式没有内建默认分区概念,需要额外子表加触发器。
  • 维护操作:声明式提供 ATTACH/DETACH PARTITION,可在线挂载或卸载分区;继承式只能靠 ALTER TABLE ... INHERIT/NO INHERIT,且不保证数据边界正确。

二、分区裁剪原理

分区裁剪(partition pruning)是分区表性能收益的核心来源。它的目标是在生成执行计划阶段,就把与查询条件不可能匹配的分区排除掉,从而减少扫描的 Relation 数量。

裁剪发生在两个层面:

  1. 计划期裁剪(plan-time pruning):当 WHERE 条件中的分区键是常量或可在计划阶段求值的表达式时,优化器直接排除不匹配分区。例如 WHERE log_date >= '2024-03-01' AND log_date < '2024-04-01',若按月分区,则只有 2024-03 那个分区会进入计划。
  2. 执行期裁剪(run-time pruning):当分区键来自参数、子查询或 PREPARE 语句的占位符时,计划阶段无法确定值,PostgreSQL 会在执行时对每个可能的分区再做一次判断。11 版本引入 run-time pruning,12 版本进一步支持在 Append 节点下动态裁剪。

裁剪能否生效,取决于分区键是否出现在可下推的谓词中。如果查询写成 WHERE date_trunc('month', log_date) = '2024-03-01',由于分区键被函数包裹,优化器无法直接比较边界,裁剪就会失效。这是实际排障中最常见的“分区表没变快”的原因之一。

声明式分区裁剪:计划期 vs 执行期 SQL 查询 WHERE log_date >= ? 分区键可求值? 常量 / 参数 / 子查询 是(常量) 计划期裁剪 plan-time pruning 优化器直接排除不匹配分区 否(参数/子查询) 执行期裁剪 run-time Append 节点动态判断 分区表:logs(PARTITION BY RANGE (log_date)) logs_2024_01 2024-01-01 ~ 02-01 logs_2024_02 2024-02-01 ~ 03-01 logs_2024_03 2024-03-01 ~ 04-01 logs_2024_04 2024-04-01 ~ 05-01 ✓ 命中扫描 ✗ 已裁剪 ✗ 已裁剪 ✗ 已裁剪 裁剪失效常见原因:分区键被函数包裹、隐式类型转换、OR 条件跨分区、统计信息缺失

三、适用版本与限制条件

声明式分区的能力随版本逐步增强,选型时必须对照目标版本:

版本 关键能力 实践影响
10 RANGE / LIST 声明式分区 无默认分区,无哈希分区,父表索引不级联
11 HASH 分区、默认分区、父表索引级联、run-time pruning 可做哈希均匀打散,可在线 ATTACH/DETACH
12 Append 下动态裁剪、分区表支持外键引用 参数化查询裁剪更彻底
13 逻辑复制支持分区表、BEFORE 行级触发器 分区表可参与逻辑复制链路
14 DETACH CONCURRENTLY、REINDEX 分区 卸载分区不再长时间持锁

限制条件同样需要提前确认:

  • 分区键必须是不可变表达式,且不能是 NULL 之外的复杂类型组合(部分类型不支持作为分区键)。
  • 唯一约束和主键必须包含全部分区键列,否则无法在父表上创建。
  • 声明式分区表不支持某些继承特性,例如父表上的 CHECK 约束不会自动下推到分区。
  • 分区数量过多会显著增加计划时间,通常建议单表分区数控制在数百到数千量级,具体取决于查询模式与硬件。
  • 继承式分区在 10 之后仍可使用,但新项目应优先声明式;已有继承式方案迁移时,需用 ATTACH PARTITION 逐表转换并校验边界。
选型建议:新项目直接使用声明式分区;若目标版本低于 10,只能采用继承式方案,并务必为每张子表建立与查询模式匹配的索引,同时开启 constraint_exclusion = partition。升级到 11 及以上后,应优先把继承式分区迁移为声明式,以换取更可靠的裁剪和更低的维护成本。

理解这两种机制的差异、裁剪的触发条件以及版本边界,才能在面对“分区表为什么没走裁剪”“升级后行为为什么变了”“该不该迁移”这类问题时,给出有依据的判断,而不是凭经验猜测。