4.6 分区决策门
分区把一个逻辑关系拆成多组物理存储。它能让按生命周期整批删除、冷热分层和特定查询裁剪非常有效,也会把唯一键、外键、索引、统计信息和运维对象成倍展开。正因为改造晚了有成本,团队常想“先分了再说”;本节用决策门阻止这种没有收益证据的确定复杂度。
4.6.1 先证明生命周期、体量或裁剪需求再决定分区
分区解决的典型问题是:
- 按月/日保留期需要快速
DROP或DETACH PARTITION,避免海量 DELETE 与 VACUUM; - 热查询稳定命中少数分区,planner 能裁剪其余分区;
- 单表/索引维护窗口、冷热存储或批量加载已经不可接受;
- 数据分布天然按 list/hash 隔离,并有清楚路由与对象数量上限。
“以后数据会很多”不在其中。行数本身也不是充分证据:一亿条窄 append-only 记录和一千万条宽、频繁更新记录的物理问题不同;内存、索引、查询选择性和保留策略都会改变拐点。官方给出的只能是宽泛经验——通常要表非常大才值得——不是一个可复制的固定阈值。
决策输入
至少收集:
current heap/index/TOAST size
daily/monthly growth
retention and legal hold
largest maintenance window
representative slow queries
predicates that can carry partition key
candidate key cardinality and null behavior
expected partition count over 3 years
backup/restore and failover objectives查询裁剪必须用计划证明:
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...
FROM candidate_partitioned_table
WHERE placed_at >= $1
AND placed_at < $2;看实际 scanned partitions、planning time、execution buffers;不要只看“有 Partition Pruning 字样”。prepared statement、parameter、函数包装和 join 条件都可能影响静态/运行时裁剪。enable_partition_pruning 还必须开启。
生命周期比“查询更快”更强
按月保留三年是可操作合同:36 个活跃分区、每月建立一个、过期时 detach/archive/drop。相比之下,“大多数查询最近数据”若没有 predicate 和计划样本,只是一句愿望。
本章订单没有保留期、法定删除批次、生产增长和慢查询数据。普通表的 PK/unique/FK 清楚且样本极小,因此不满足进入门。先不分区不是缺少架构,而是证据导向的物理决策。
4.6.2 分区键与主键、唯一约束必须共同设计
PostgreSQL 分区表上的 unique/PK 由各 partition 的本地索引实现。为了保证不同 partition 之间不重复,unique/PK 的列必须包含全部 partition key columns,而且 partition key 不能是 expression/function。
假设按 placed_at 月分区:
CREATE TABLE shop.sales_order_p (...)
PARTITION BY RANGE (placed_at);当前约束:
PRIMARY KEY (order_id)
UNIQUE (order_no)
UNIQUE (customer_id, request_key)不能原样成为 partitioned parent 的全局约束,因为它们都不含 placed_at。可选方向各有语义代价:
- 改成
(order_id, placed_at)等复合键; - 接受 only-per-partition uniqueness;
- 另建未分区 registry 表维护全局键;
- 由应用/trigger 维护跨 partition 唯一性,并承担并发正确性;
- 换一个能同时服务生命周期与唯一性的 partition key;
- 不分区。
把 placed_at 机械加入每个 unique 不等于问题解决。订单号原本承诺全局唯一,加入时间后两个 partition 可拥有同一个 order_no;除非 API 唯一性合同也改为 (order_no, placed_at),语义已经变了。
NULL 与草稿
v1 的 draft order placed_at IS NULL。range partitioning 对 NULL 没有普通 range 归属,需要 default partition 或不同 key。若用 created_at 路由,保留期可能与业务 placed/paid 生命周期不一致。分区键必须同时满足:
- 每行插入时可用、稳定;
- 业务生命周期/删除批次;
- 高频查询 predicate;
- unique/PK 与 FK 形状;
- 更新是否会导致跨 partition row movement。
一个“时间列存在”远不足以当分区键。
4.6.3 外键、引用方式与未来在线改造代价
若 order PK 从 (order_id) 变成 (order_id, placed_at),引用它的 order item 与 payment 通常也要携带 placed_at:
sales_order_item(order_id, order_placed_at) -> sales_order(...)
payment(order_id, order_placed_at) -> sales_order(...)这增加键宽、索引宽、写入参数和更新路由;draft 的 NULL 又使引用更复杂。保留一个未分区 key registry 可避免传播时间键,却新增一张强一致写入热点与生命周期协调表。两种都不是免费。
PostgreSQL 已支持针对 partitioned table 的外键,但被引用键仍要满足 partitioned unique/PK 限制。应用 ORM “支持分区”也不能绕过数据库这一事实。
普通表不能原地变成分区表
官方文档明确:不能把 regular table 直接切换成 partitioned table,反之亦然。常见迁移需要:
- 新建 partitioned parent 与 partitions;
- 建立等价列、约束、索引、权限、trigger 与注释;
- backfill 历史数据;
- 捕获 backfill 期间增量(短暂停写、dual-write、trigger 或逻辑复制);
- 验证行数、checksum、FK 与查询计划;
- 短锁窗口切换名称/view/service;
- 保留前滚/回退与旧表清理门。
ATTACH PARTITION 可复用已经装载的普通表,但需要证明 partition constraint;没有匹配 CHECK 时会扫描验证并持有相应锁。partitioned index 也有自己的并发创建/attach 流程。ch26 会完整演练,本章只估算设计后果。
现在不分区,也要为未来保留边界
不应把应用 SQL 绑定具体 child table;所有读写面向逻辑 relation/view。业务标识不要编码当前 partition 名。持续记录时间分布、表/索引体积和保留期,让未来迁移有数据。
但不要为了“方便未来”现在就把 partition key 传播到所有 API:这会立刻锁定尚未证明的设计。可演进的关键是清楚接口与可验证迁移,不是提前暴露物理细节。
4.6.4 产出“现在分区 / 暂不分区”的可复查 ADR
ADR-004
decision: ch04-v1 remains unpartitioned
status: accepted
review chapter: ch26主要理由不是“数据还小”一句话,而是:
- 没有体量、增长、保留期或慢查询证据;
- 当前 order_id/order_no/request key 要求全局唯一;
- order item/payment 通过 FK 引用订单;
- 候选
placed_at对 draft 为 NULL; - 预分区会立即增加对象、维护和恢复复杂度。
触发复查
任一条件由真实证据满足时复查:
- heap/index 已使单表维护窗口不可接受;
- 有稳定、可按候选 key 整批执行的过期/归档政策;
- representative query 携带 key,计划证明 pruning 收益;
- 普通表无法满足写入、备份恢复或冷热分层目标。
复查包必须包含增长率、关系/索引大小、保留期、慢查询计划、候选键、预期 partition count,以及一次迁移/回退演练。结论可以仍是“暂不分区”;ADR 的价值是让新证据能推翻旧决定。
数据库验收
verify-v1.sql 不只在文档中说“不分区”,还检查五表都没有 pg_partitioned_table entry:
SELECT
c.oid::regclass,
c.relkind
FROM pg_catalog.pg_class AS c
WHERE c.oid IN (
'shop.customer'::regclass,
'shop.product'::regclass,
'shop.sales_order'::regclass,
'shop.sales_order_item'::regclass,
'shop.payment'::regclass
);预期 relkind='r',状态摘要输出:
partition_decision=not-now如果有人私自把某表换成 partitioned hierarchy,verify 失败,迫使代码与 ADR 一起评审。
ADR 模板
context
measured evidence
candidate keys and alternatives
PK/UK/FK consequences
query/lifecycle benefits
object and operation costs
decision and owner
review triggers/date
migration and rollback outline不要记录“PostgreSQL 支持 range partition”这类产品事实;记录为什么这个模型在这个时点选择什么,以及什么证据会让决定失效。
本节验收
- 没有使用单一行数阈值替代体积、生命周期与计划证据;
- 候选 partition key 同时审查 NULL、稳定性、查询与保留期;
- 能解释为什么 PG partitioned unique 必须包含全部 key;
- order_no、request key 与 child FK 的语义后果已列出;
- 知道 regular→partitioned 不是原地 ALTER,迁移需新结构和切换;
- ADR 有 owner、反对方案、复查触发条件和数据库 verify;
- “暂不分区”被当成可复查的积极决定。
参考资料
- PostgreSQL 18:table partitioning
- PostgreSQL 18:partition pruning
- PostgreSQL 18:CREATE TABLE 的 partition/unique 限制
- PostgreSQL 18:ALTER TABLE / ATTACH PARTITION
上一节:类型与约束的物理代价 · 返回本章目录 · 下一节:实战:把逻辑模型落成可靠物理模式 · 查看全书目录 · 查看索引中心