11.4 在线分区化
本书对分区采用渐进式教学:
本节不推翻 ch04 对订单模型“暂不分区”的 ADR。我们使用独立事件 fixture,学习当证据门已经通过后,如何把预装数据表挂入/摘出分区层次,并把 lock、scan、version 和 recovery 语义说清楚。
11.4.1 从 ch04 的分区 ADR 选择迁移路径
先确认旧决定为什么失效
ch04 分区决策门 要求至少出现一类真实证据:
- retention/归档需要按整批数据处理;
- relation/index 规模已形成可测瓶颈;
- 代表性查询能稳定携带 partition key,并证明 pruning 收益;
- 热冷分层、备份或维护窗口需要独立 physical units。
进入 ch11 时,不应把“学会 ATTACH”误解为“现在应该分区”。先更新 ADR:
如果新证据仍不足,正确结论可以继续是 not-now。
Regular table 不能原地变成 partitioned table
迁移通常需要新结构。常见路径:
路径 B 适合:
- 已经按边界离线装载的一批数据;
- 归档表重新加入 logical parent;
- 新周期分区先单独 load/validate;
- 迁移时把一段现有数据转换成 child。
它不是把任意一张巨大表“瞬间变成分区表”。父/子 column shape、constraints、indexes、owner、storage 与数据 bound 都要准备。
Key、unique 与 FK 必须先重做语义
PostgreSQL partitioned table 的 unique/primary key 必须包含全部 partition key columns,且 key 不能含 expression。因为 uniqueness 最终由各 child 的本地 index 执行,数据库要从 partition routing 推导跨 child 不重复。
如果旧合同是:
按 placed_at 分区后机械改为:
不再保证 order_no 全局唯一。两个不同月份可以重复 order number。可选设计:
- 改 API identity 为复合键,明确语义变化;
- 保留未分区 global registry;
- 选择不同 partition key;
- 接受只在 partition 内唯一;
- 不分区。
外键、idempotency key、upsert conflict target 和 driver 参数都会受影响。没有解决这些逻辑问题时,在线 DDL 技巧没有意义。
本章 fixture 的范围
事件表没有 global unique/FK,只演练 range bound:
独立候选 ch11_event_2025q1 有 20,000 行。数据都落在:
这个简化让实验精确关注 attach/detach;它不是完整生产事件模型。
11.4.2 预建约束、ATTACH 与扫描规避
ATTACH 的两种验证路径
命令:
PostgreSQL 必须证明所有 child row 满足隐含 partition bound。若没有可用证明,它会在持有 child ACCESS EXCLUSIVE 时扫描该表。
预先建立并验证匹配 CHECK:
官方文档建议这样让系统跳过 attach validation scan。注意:
- constraint 必须 valid;
- expression 必须足以让系统证明相同 bound;
- time zone/type/cast/NULL 语义要一致;
- child columns 必须与 parent 完全匹配;
- child 若自身 partitioned,递归 subpartition 也要考虑;
- attach 仍会取得锁,预建 CHECK 不等于 lock-free。
实测 ATTACH 的锁
partition_lab.py 在一个 transaction 中执行 ATTACH 后故意暂不 commit,observer 查询 holder 的 pg_locks。
PostgreSQL 18.6 结果:
| Relation | Granted mode |
|---|---|
ch11_event parent |
ShareUpdateExclusiveLock |
ch11_event_2025q1 child |
AccessExclusiveLock |
这与 PostgreSQL 18 ALTER TABLE 文档一致:
预验证 CHECK 降低的是持有 child 强锁期间的扫描工作,不改变 child 需要强锁这一事实。若 child 仍被报表长读,ATTACH 仍可能等锁或阻塞。
本地证据能证明到哪里
实验保存:
relfilenode 未变说明数据没有 copy/rewrite,但不能单独证明“没有 validation scan”。“匹配 valid CHECK 可避免扫描”来自官方语义;运行证据证明我们确实提供了该 CHECK 和目标 bound。
不要用一个极快 elapsed time 声称 scan 被证明跳过:数据可能在 cache 中、表可能太小、计时噪声也可能掩盖差异。
Default partition 的隐藏扫描
如果 parent 已有 DEFAULT partition,添加一个新显式 range 时,PostgreSQL 还必须证明 DEFAULT 中没有属于新 range 的行。没有排除性 CHECK 时会:
若 DEFAULT 自身 partitioned,会递归检查。发布计划要么:
- 预先为 DEFAULT 添加排除新 range 的 valid CHECK;
- 先迁出该 range 的行;
- 在可接受窗口完成扫描;
- 重新设计是否需要 DEFAULT。
只优化待 attach child 而忘记 DEFAULT,是常见的“测试环境快、生产突然锁住”原因。
Partitioned index 的在线路径不同
不能直接对 partitioned parent 使用:
官方推荐的渐进路径:
每个 child index 要验证定义、opclass、collation、validity 与 ownership。对 unique/PK 还要满足 partition key 规则。生产实施还要把这套检查扩展到全部现存分区,并验证新增分区不会漏建索引。
11.4.3 DETACH、并发能力与锁等级必须按版本说明
普通 DETACH 与 CONCURRENTLY
普通形式对 parent 请求 ACCESS EXCLUSIVE。PostgreSQL 14 引入:
并发形式对 parent 使用较低的 SHARE UPDATE EXCLUSIVE,内部有两个 transaction:
- parent 与 child 取得
SHARE UPDATE EXCLUSIVE,标记正在 detach 并 commit; - 等待使用该 partitioned table 的旧 transaction 结束;
- 再取得 parent
SHARE UPDATE EXCLUSIVE和 childACCESS EXCLUSIVE; - 完成 detach,并建立等价 CHECK。
这解释了两个边界:
- 它仍可能等待长 transaction;
- 最终仍要短时取得 child
ACCESS EXCLUSIVE。
“CONCURRENTLY”不是不等待、不锁或固定秒数完成。
事务与结构限制
DETACH PARTITION ... CONCURRENTLY:
- 不能在 transaction block 中运行;
- parent 有 DEFAULT partition 时不允许;
- interrupted/pending detach 可能需要
FINALIZE; - 目标版本必须 PostgreSQL 14+;
- FK、subpartition 和 concurrent activity 仍需按目标版本复核。
因此 migration runner 需要像 concurrent index 一样提供独立 non-transactional command。
本章的 Python runner 用两个 psql top-level command:
它们在同一 session 执行,但 DETACH 不在显式或 implicit multi-statement transaction 内。
版本事实不能倒推
本书支持 PG14–18,因此该语法在主体矩阵中可用。若维护 PG13:
不能运行时 fallback 成普通 DETACH 后仍报告相同 lock semantics。应:
- 明确跳过并报告 unsupported;
- 选择有维护窗的 ordinary detach;
- 或先升级目标 major。
本章 manifest 保存 server_version_num=180006,review 要求 >=140000;它不把 18.6 的 timing 当成 PG14–17 保证。
11.4.4 产出一次可回退的分区化发布
完整演练
实验状态:
这里“回退”是结构性 reattach,因为 detach 后没有允许 child 与 parent 的写路径发生分叉。若 detach 后:
- child 单独接收写入;
- parent 在同一 bound 又建立新 partition;
- constraint 被修改;
- indexes/privileges/schema 发生漂移;
reattach 就不再是机械 undo,需要 reconciliation 和新的 lock/scan 评审。
发布 runbook
生产 attach 前:
detach 前:
Detach 不等于删除
DETACH 保留 table 和数据,适合:
- 归档前
COPY/backup; - 低频报表;
- 数据压缩/汇总;
- 独立验证;
- 在明确边界下重新 attach。
DROP TABLE partition 则删除数据对象。不要在同一个“一键 retention”里把 detach、archive verification 和 drop 混成无法暂停的动作。
API 不应暴露 child identity
应用继续访问 logical parent:
不要让业务 URL、job payload 或 ORM model 绑定 event_2025q1。否则每次 attach/detach 都变成 application contract 变更,物理生命周期无法独立演进。
本节验收问题
- ch04 ADR 的进入门是否真的被新证据触发;
- partition key 是否同时满足 lifecycle、query、NULL 与稳定性;
- global unique/PK/FK 语义是否重新设计;
- regular→partitioned 是否有新结构和切换路径;
- candidate child column shape 是否与 parent 完全一致;
- matching bound CHECK 是否 valid 且可被系统证明;
- ATTACH 的 parent/child/default locks 是否按目标版本写明;
- DEFAULT partition 的排除扫描是否处理;
- partitioned index 是否使用 per-child concurrent build/attach;
- DETACH CONCURRENTLY 是否 PG14+ 且位于 transaction block 外;
- pending detach/FINALIZE 与长 transaction 是否进入故障预案;
- detach 后写入是否可能让 reattach 失去可逆性;
- archive verification 与 destructive drop 是否分开授权;
- 应用是否只依赖 logical parent。
参考资料
- PostgreSQL 18:Table Partitioning
- PostgreSQL 18:ALTER TABLE ATTACH/DETACH
- PostgreSQL 14 Release Notes:Concurrent Detach
上一节:索引与约束的在线化路径 · 返回本章目录 · 下一节:数据回填与流量切换 · 查看全书目录 · 查看索引中心