11.4 在线分区化
本书对分区采用渐进式教学:
ch04: decide whether partitioning is justified
ch11: rehearse attach/detach as a safe schema release
ch26: operate the full partition lifecycle at production scale本节不推翻 ch04 对订单模型“暂不分区”的 ADR。我们使用独立事件 fixture,学习当证据门已经通过后,如何把预装数据表挂入/摘出分区层次,并把 lock、scan、version 和 recovery 语义说清楚。
11.4.1 从 ch04 的分区 ADR 选择迁移路径
先确认旧决定为什么失效
ch04 分区决策门 要求至少出现一类真实证据:
- retention/归档需要按整批数据处理;
- relation/index 规模已形成可测瓶颈;
- 代表性查询能稳定携带 partition key,并证明 pruning 收益;
- 热冷分层、备份或维护窗口需要独立 physical units。
进入 ch11 时,不应把“学会 ATTACH”误解为“现在应该分区”。先更新 ADR:
old evidence
new evidence and timestamp
candidate key and partition count
unique/PK/FK implications
NULL and row-movement semantics
query/pruning measurements
retention and operational owner
migration and rollback strategy如果新证据仍不足,正确结论可以继续是 not-now。
Regular table 不能原地变成 partitioned table
迁移通常需要新结构。常见路径:
A. new parent + new partitions
→ copy/backfill
→ catch up writes
→ switch logical name/view
B. prepare an existing regular table
→ prove it fits one bound
→ ATTACH PARTITION
C. new partitioned table + logical replication/CDC
→ reconcile
→ cut over路径 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 不重复。
如果旧合同是:
UNIQUE(order_no)按 placed_at 分区后机械改为:
UNIQUE(order_no, 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:
CREATE TABLE shop_private.ch11_event (
event_id bigint NOT NULL,
occurred_at timestamptz NOT NULL,
payload text NOT NULL
) PARTITION BY RANGE (occurred_at);独立候选 ch11_event_2025q1 有 20,000 行。数据都落在:
[2025-01-01 00:00:00+00, 2025-04-01 00:00:00+00)这个简化让实验精确关注 attach/detach;它不是完整生产事件模型。
11.4.2 预建约束、ATTACH 与扫描规避
ATTACH 的两种验证路径
命令:
ALTER TABLE shop_private.ch11_event
ATTACH PARTITION shop_private.ch11_event_2025q1
FOR VALUES FROM ('2025-01-01 00:00:00+00')
TO ('2025-04-01 00:00:00+00');PostgreSQL 必须证明所有 child row 满足隐含 partition bound。若没有可用证明,它会在持有 child ACCESS EXCLUSIVE 时扫描该表。
预先建立并验证匹配 CHECK:
ALTER TABLE shop_private.ch11_event_2025q1
ADD CONSTRAINT ch11_event_2025q1_bound
CHECK (
occurred_at >=
timestamptz '2025-01-01 00:00:00+00'
AND occurred_at <
timestamptz '2025-04-01 00:00:00+00'
);官方文档建议这样让系统跳过 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.4 结果:
| Relation | Granted mode |
|---|---|
ch11_event parent | ShareUpdateExclusiveLock |
ch11_event_2025q1 child | AccessExclusiveLock |
这与 PostgreSQL 18 ALTER TABLE 文档一致:
parent: SHARE UPDATE EXCLUSIVE
attached table: ACCESS EXCLUSIVE
default partition (if any): ACCESS EXCLUSIVE预验证 CHECK 降低的是持有 child 强锁期间的扫描工作,不改变 child 需要强锁这一事实。若 child 仍被报表长读,ATTACH 仍可能等锁或阻塞。
本地证据能证明到哪里
实验保存:
bound CHECK convalidated=true before attach
child row count=20,000
exact ATTACH lock modes
child relfilenode unchanged
parent count=20,000 after attach
pg_inherits edge=1
relispartition=truerelfilenode 未变说明数据没有 copy/rewrite,但不能单独证明“没有 validation scan”。“匹配 valid CHECK 可避免扫描”来自官方语义;运行证据证明我们确实提供了该 CHECK 和目标 bound。
不要用一个极快 elapsed time 声称 scan 被证明跳过:数据可能在 cache 中、表可能太小、计时噪声也可能掩盖差异。
Default partition 的隐藏扫描
如果 parent 已有 DEFAULT partition,添加一个新显式 range 时,PostgreSQL 还必须证明 DEFAULT 中没有属于新 range 的行。没有排除性 CHECK 时会:
scan default partition
while holding ACCESS EXCLUSIVE on it若 DEFAULT 自身 partitioned,会递归检查。发布计划要么:
- 预先为 DEFAULT 添加排除新 range 的 valid CHECK;
- 先迁出该 range 的行;
- 在可接受窗口完成扫描;
- 重新设计是否需要 DEFAULT。
只优化待 attach child 而忘记 DEFAULT,是常见的“测试环境快、生产突然锁住”原因。
Partitioned index 的在线路径不同
不能直接对 partitioned parent 使用:
CREATE INDEX CONCURRENTLY ON partitioned_parent (...);官方推荐的渐进路径:
CREATE INDEX ON ONLY parent # invalid parent index
→ CREATE INDEX CONCURRENTLY child1
→ ALTER INDEX parent_idx ATTACH PARTITION child1_idx
→ repeat for every child
→ parent index becomes valid when complete每个 child index 要验证定义、opclass、collation、validity 与 ownership。对 unique/PK 还要满足 partition key 规则。ch26 将在多分区规模下完整执行。
11.4.3 DETACH、并发能力与锁等级必须按版本说明
普通 DETACH 与 CONCURRENTLY
ALTER TABLE parent
DETACH PARTITION child;普通形式对 parent 请求 ACCESS EXCLUSIVE。PostgreSQL 14 引入:
ALTER TABLE parent
DETACH PARTITION child CONCURRENTLY;并发形式对 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:
SET ROLE pg36_owner
ALTER TABLE ... DETACH PARTITION ... CONCURRENTLY它们在同一 session 执行,但 DETACH 不在显式或 implicit multi-statement transaction 内。
版本事实不能倒推
本书支持 PG14–18,因此该语法在主体矩阵中可用。若维护 PG13:
CONCURRENTLY / FINALIZE unavailable不能运行时 fallback 成普通 DETACH 后仍报告相同 lock semantics。应:
- 明确跳过并报告 unsupported;
- 选择有维护窗的 ordinary detach;
- 或先升级目标 major。
本章 manifest 保存 server_version_num=180004,review 要求 >=140000;它不把 18.4 的 timing 当成 PG14–17 保证。
11.4.4 产出一次可回退的分区化发布
完整演练
实验状态:
before:
child standalone
valid bound CHECK
rows=20,000
parent rows=0
relispartition=false
attach:
capture parent/child locks before commit
commit
parent rows=20,000
relispartition=true
detach concurrently:
top-level PG14+ command
child rows=20,000
parent rows=0
relispartition=false
filenode unchanged
rollback rehearsal:
verify data/bound still valid
reattach
final:
parent rows=20,000
child attached
filenode unchanged throughout这里“回退”是结构性 reattach,因为 detach 后没有允许 child 与 parent 的写路径发生分叉。若 detach 后:
- child 单独接收写入;
- parent 在同一 bound 又建立新 partition;
- constraint 被修改;
- indexes/privileges/schema 发生漂移;
reattach 就不再是机械 undo,需要 reconciliation 和新的 lock/scan 评审。
发布 runbook
生产 attach 前:
1 verify target database/primary/role/search_path
2 verify parent/child identity and exact column shape
3 freeze or control writers to standalone child
4 validate bound CHECK and count violations=0
5 build/validate child indexes and constraints
6 handle DEFAULT exclusion
7 measure relation/index size and active transactions
8 set lock/statement timeout
9 observe service SLI, WAL, lag, disk
10 ATTACH
11 verify pg_inherits, relispartition, rows, pruning and privileges
12 keep standalone recovery plan until observation completesdetach 前:
1 identify PG major and DEFAULT restriction
2 decide ordinary vs concurrently
3 check long transactions/snapshots
4 define routing after detach
5 execute top-level command
6 inspect pending/final state
7 verify row count and new CHECK
8 archive/copy/drop only as separate destructive actionDetach 不等于删除
DETACH 保留 table 和数据,适合:
- 归档前
COPY/backup; - 低频报表;
- 数据压缩/汇总;
- 独立验证;
- 在明确边界下重新 attach。
DROP TABLE partition 则删除数据对象。不要在同一个“一键 retention”里把 detach、archive verification 和 drop 混成无法暂停的动作。
API 不应暴露 child identity
应用继续访问 logical parent:
SELECT ...
FROM event
WHERE occurred_at >= $1
AND occurred_at < $2;不要让业务 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
上一节:索引与约束的在线化路径 · 返回本章目录 · 下一节:数据回填与流量切换 · 查看全书目录 · 查看索引中心