跳至内容

4.6 分区决策门

分区把一个逻辑关系拆成多组物理存储。它能让按生命周期整批删除、冷热分层和特定查询裁剪非常有效,也会把唯一键、外键、索引、统计信息和运维对象成倍展开。正因为改造晚了有成本,团队常想“先分了再说”;本节用决策门阻止这种没有收益证据的确定复杂度。

4.6.1 先证明生命周期、体量或裁剪需求再决定分区

分区解决的典型问题是:

  • 按月/日保留期需要快速 DROPDETACH 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。可选方向各有语义代价:

  1. 改成 (order_id, placed_at) 等复合键;
  2. 接受 only-per-partition uniqueness;
  3. 另建未分区 registry 表维护全局键;
  4. 由应用/trigger 维护跨 partition 唯一性,并承担并发正确性;
  5. 换一个能同时服务生命周期与唯一性的 partition key;
  6. 不分区。

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,反之亦然。常见迁移需要:

  1. 新建 partitioned parent 与 partitions;
  2. 建立等价列、约束、索引、权限、trigger 与注释;
  3. backfill 历史数据;
  4. 捕获 backfill 期间增量(短暂停写、dual-write、trigger 或逻辑复制);
  5. 验证行数、checksum、FK 与查询计划;
  6. 短锁窗口切换名称/view/service;
  7. 保留前滚/回退与旧表清理门。

ATTACH PARTITION 可复用已经装载的普通表,但需要证明 partition constraint;没有匹配 CHECK 时会扫描验证并持有相应锁。partitioned index 也有自己的并发创建/attach 流程。ch26 会完整演练,本章只估算设计后果。

现在不分区,也要为未来保留边界

不应把应用 SQL 绑定具体 child table;所有读写面向逻辑 relation/view。业务标识不要编码当前 partition 名。持续记录时间分布、表/索引体积和保留期,让未来迁移有数据。

但不要为了“方便未来”现在就把 partition key 传播到所有 API:这会立刻锁定尚未证明的设计。可演进的关键是清楚接口与可验证迁移,不是提前暴露物理细节。

4.6.4 产出“现在分区 / 暂不分区”的可复查 ADR

partition-adr.md记录:

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;
  • 预分区会立即增加对象、维护和恢复复杂度。

触发复查

任一条件由真实证据满足时复查:

  1. heap/index 已使单表维护窗口不可接受;
  2. 有稳定、可按候选 key 整批执行的过期/归档政策;
  3. representative query 携带 key,计划证明 pruning 收益;
  4. 普通表无法满足写入、备份恢复或冷热分层目标。

复查包必须包含增长率、关系/索引大小、保留期、慢查询计划、候选键、预期 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;
  • “暂不分区”被当成可复查的积极决定。

参考资料


上一节:类型与约束的物理代价 · 返回本章目录 · 下一节:实战:把逻辑模型落成可靠物理模式 · 查看全书目录 · 查看索引中心

最后更新于