上周有个 DBA 朋友在群里吐槽:月底归档要把 12 个月分区合并成 4 个季度分区,DETACH、CREATE、INSERT、DROP 五步走,跑了一晚上,WAL 堆了 40GB,主从延迟飙到 20 分钟。我问他升级了没,他说还在 PG 16。
PG 19 终于把分区合并和拆分写进了一条命令。MERGE PARTITIONS 把多个分区合成一个,SPLIT PARTITION 把一个分区拆成多个。不用 DETACH,不用导出,不用改名,不用改约束。
Andrew Dunstan(EDB)在 8 月 22 日的 pgsql-hackers 邮件里说得很直白:PG 19 的 blockbuster 特性就两个,一个是 REPACK,另一个是 pg_plan_advice。但他同时把 MERGE/SPLIT PARTITIONS 补进了"主要特性"清单——Jonathan Katz 提交的 v2 补丁明确指出这是一个"历史性遗漏"。
这条命令看起来简单,背后踩了一整个 Beta 周期的坑。今天拆透它。
1 老办法有多痛
假设有一张按月分区的订单表:
CREATE TABLE orders (
id bigint,
order_date date,
amount numeric
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_202601 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_202602 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE orders_202603 PARTITION OF orders
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
月底要把 Q1 三个月合成一个分区。PG 18 及以前的操作链:
-- Step 1: DETACH 三个分区
ALTER TABLE orders DETACH PARTITION orders_202601;
ALTER TABLE orders DETACH PARTITION orders_202602;
ALTER TABLE orders DETACH PARTITION orders_202603;
-- Step 2: 创建新的季度分区
CREATE TABLE orders_2026q1 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
-- Step 3: 从三个旧表导入数据
INSERT INTO orders_2026q1
SELECT * FROM orders_202601
UNION ALL
SELECT * FROM orders_202602
UNION ALL
SELECT * FROM orders_202603;
-- Step 4: 删除旧表
DROP TABLE orders_202601;
DROP TABLE orders_202602;
DROP TABLE orders_202603;
-- Step 5: 如果有索引、约束、触发器,全部在新分区上重建
CREATE INDEX idx_orders_q1_date ON orders_2026q1 (order_date);
五个步骤。每一步都是 DDL 或大量数据搬移。DETACH 要拿 ACCESS EXCLUSIVE 锁,INSERT 生成等量 WAL,DROP 还得清理物理文件。一张 5000 万行的月分区,三个合并起来跑一两个小时很正常。
更要命的是逻辑复制。DETACH 之后的 INSERT 在订阅端是普通写入,如果发布端没有配好副本标识,数据一致性根本没法保证。
2 新命令:一条搞定
PG 19 把上面五步压成一条:
-- 合并三个分区为一个
ALTER TABLE orders
MERGE PARTITIONS (orders_202601, orders_202602, orders_202603)
INTO orders_2026q1;
反过来,把一个季度分区拆成三个月的:
ALTER TABLE orders SPLIT PARTITION orders_2026q1 INTO (
PARTITION orders_202601 FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'),
PARTITION orders_202602 FOR VALUES FROM ('2026-02-01') TO ('2026-03-01'),
PARTITION orders_202603 FOR VALUES FROM ('2026-03-01') TO ('2026-04-01')
);
两条命令,数据原地搬家,不用导出导入。
3 底层干了什么
MERGE PARTITIONS 的执行过程:
- 以分区表本身为模板,创建新分区
orders_2026q1 - 继承父表的访问方法、持久化类型、表空间
- 把三个源分区的数据搬进新分区
- 数据迁移完成后,创建索引和 identity 列
- 以 RESTRICT 模式删除三个旧分区
关键细节:
- 约束、列默认值、生成列表达式、identity 列、索引、触发器会被复制到新分区
- 扩展统计和安全策略不会被复制
- 索引在数据迁移完成后才创建,不是边搬边建
锁的行为也要注意。MERGE 在父表上拿 ACCESS EXCLUSIVE 锁,在被合并的分区上也是 ACCESS EXCLUSIVE。整个操作期间,这两张表都不能读写。
SPLIT 的机制几乎一样,只是方向相反:创建多个新分区,把源分区数据按边界分到各新分区,最后删掉源分区。
4 被合并分区必须相邻
Range 分区表做 MERGE,被合并的分区的范围必须相邻。哪怕没有 DEFAULT 分区,这条规则也适用。
-- 可以:202601 和 202602 相邻
ALTER TABLE orders MERGE PARTITIONS (orders_202601, orders_202602)
INTO orders_2026h1;
-- 报错:202601 和 202603 不相邻
ALTER TABLE orders MERGE PARTITIONS (orders_202601, orders_202603)
INTO orders_2026xx;
-- ERROR: partition "orders_202603" is not adjacent to "orders_202601"
List 分区表没有相邻限制,但所有被合并分区的边界会合并成新分区的边界。
如果 DEFAULT 分区在合并列表里,新分区就成为新的 DEFAULT 分区。这个设计很合理——DEFAULT 本来就没有固定边界。
5 不支持 Hash 分区
这是硬限制。Hash 分区的数据分布靠哈希函数,合并或拆分会改变哈希空间,导致数据路由全乱。官方文档写得很明确:
Hash-partitioned target table is not supported.
还有一条:只能操作简单分区(非子分区化的叶子分区)。如果你的分区本身又是一个分区表,MERGE 和 SPLIT 都不能用。
6 Beta 期间踩的三个大坑
这条命令在 Beta 阶段经历了一整个修复周期。Percona 的 Zsolt Parragi 在 7 月 23 日报告了五个问题,其中三个值得单独讲。
坑一:生成列被静默重算
这是最严重的一个。假设父表的生成列表达式是 g = id * 2,但某个分区的生成列是 g = id * 100(通过 ATTACH 带不同定义的分区)。MERGE 时,数据搬进新分区会用父表的表达式重新计算 g。
结果:一条 id = 3, g = 300 的行,MERGE 后变成 id = 3, g = 6。数据变了,没有任何报错。
Alexander Korotkov(Supabase,该特性的维护者)在 8 月 5 日给出了最终方案:只允许源分区与父表生成列表达式一致时才执行 MERGE/SPLIT。不一致直接拒绝。
补丁 v3 进一步加了 checkPartitionSystemColumnRefs() 函数,拒绝任何引用系统列(比如 tableoid)的生成列和 CHECK 约束。因为 jian he 发现可以用 NULLIF(tableoid, 18470) 绕过检查。
坑二:RLS 策略被静默丢弃
Melanie Plageman 在 8 月 12 日指出:如果一个分区有行级安全策略,SPLIT 之后新分区没有这个策略。原来 Carol 在父表查不到的秘密行,直接查新分区就能看到。
MERGE 也有类似问题:两个分区有相同的 RLS 策略,合并后的新分区反而没有。
最终方案(v4 补丁):直接拒绝带 RLS 的分区执行 MERGE/SPLIT。错误信息:
ERROR: cannot merge or split partition "tp_0_2"
that has row-level security enabled
DETAIL: Row-level security is not carried over to the new partition,
which would expose rows that the partition currently hides.
HINT: Disable row-level security on the partition before the operation,
and re-establish it on the new partition afterwards.
jian he 在 8 月 20 日还建议改进 HINT 措辞——光 disable RLS 不够,还得删掉并重建策略。
坑三:逻辑复制数据丢失
MERGE 把数据搬进新分区时,这些行以普通堆插入方式写入。逻辑解码会把它们当作新分区上的 INSERT,但没有匹配的 DELETE。
更麻烦的是订阅端的传播链路:
- 发布端执行 MERGE
- 订阅端还没来得及处理,发布端对已合并行做了 UPDATE
- 订阅端恢复时发现新分区不在订阅中,丢弃了这个 UPDATE
- 用户执行 REFRESH PUBLICATION,之前的更新丢失
v5 补丁的解决方案:MERGE/SPLIT 期间不对新分区的行插入做逻辑解码,视为 DDL 操作而非数据变更。同时保留副本标识和发布成员资格,不匹配时报错。
7 修复哲学:遇到不一致就拒绝
看完上面三个坑,能发现一个共同模式:PG 19 的修复策略是遇到属性不一致就直接报错,而不是尝试自动处理。
Daniel Gustafsson 在 8 月 14 日说得清楚:RLS 和 ACL 属于"明显危险"的静默丢弃项。他建议只在所有参数与父表一致可保留时才允许 MERGE。
Melanie Plageman 也指出:在 19 中静默丢弃属性、然后在 20 中开始自动传播,没有意义——会困扰为 19 编写重建脚本的用户。
Alexander Korotkov 在 8 月 19 日的表态最关键:
“I think this is the way to save this feature for pg19.”
翻译:遇到分歧就报错,是保住这个特性进 PG 19 的唯一办法。自动复制 RLS、重算生成列这些功能留给 PG 20。
Nathan Bossart 对就绪度有担忧——补丁规模大、问题还不少。但方向已经明确:以受限形式进 19,完整功能推到 20+。
8 实战建议
什么时候用 MERGE/SPLIT
- 月分区合季度分区、季度合年度:数据量不大(百万级),操作秒级完成
- 冷数据归档合并:把历史小分区合成大分区减少分区数
- DEFAULT 分区太大需要拆分:SPLIT 把杂乱数据按边界归到新分区
什么时候别用
- 大表(GB 级以上):MERGE 要移动全部数据,耗时很长。官方明确不建议用该命令合并非常大的分区
- 有 RLS 策略的分区:PG 19 直接拒绝执行
- 生成列表达式与父表不一致:会被拒绝
- Hash 分区表:不支持
- 逻辑复制环境:确认发布端配的是分区根(FOR TABLE ALL),不是单个分区。否则 REFRESH PUBLICATION 可能丢数据
操作前检查清单
-- 1. 检查待合并分区是否有 RLS
SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE relname IN ('orders_202601', 'orders_202602', 'orders_202603');
-- 2. 检查生成列表达式是否与父表一致
SELECT attrelid::regclass AS table_name, attname, attgenerated,
pg_get_expr(adbin, adrelid) AS expression
FROM pg_attribute a
JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attgenerated = 's'
AND a.attrelid IN (
'orders'::regclass,
'orders_202601'::regclass,
'orders_202602'::regclass
);
-- 3. 确认分区类型不是 Hash
SELECT partstrat FROM pg_partitioned_table
WHERE partrelid = 'orders'::regclass;
-- 返回 'r' = Range, 'l' = List, 'h' = Hash
-- 4. 检查逻辑复制发布是否包含分区根
SELECT pubname, puballtables FROM pg_publication
WHERE pubname = 'my_pub';
9 与 DETACH 方案对比
| 维度 | DETACH+INSERT+DROP(老办法) | MERGE PARTITIONS(PG 19) |
|---|---|---|
| 操作步数 | 5 步 | 1 条命令 |
| 数据搬移 | INSERT 生成等量 WAL | 内部搬移,行为类似 CLUSTER |
| 索引/约束 | 手动在新分区重建 | 自动从父表模板复制 |
| 逻辑复制 | DETACH 后 INSERT 破坏复制 | Beta 3 修复为 DDL 不解码 |
| RLS 策略 | 需手动重建 | PG 19 直接拒绝有 RLS 的分区 |
| 生成列 | 需手动确认表达式一致 | PG 19 校验不一致就拒绝 |
| 锁 | DETACH 和 INSERT 各拿锁 | 父表和源分区 ACCESS EXCLUSIVE |
| 大表性能 | 可分批处理 | 不建议用于大表 |
| 回滚 | 中间步骤可回滚 | 单事务原子性 |
老办法的优势是灵活——可以分批搬、可以中断、可以中途检查。新命令的优势是简洁和原子性——要么全做,要么全不做。
10 局限与不适用场景
MERGE/SPLIT PARTITIONS 在 PG 19 是第一版,限制不少:
- 不支持 Hash 分区,未来也不太可能支持(哈希空间变了数据路由全乱)
- 只能操作叶子分区,子分区化的分区不能直接 MERGE/SPLIT
- 有 RLS 策略的分区直接拒绝执行
- 生成列表达式必须与父表一致,否则拒绝
- 扩展统计和安全策略不会被复制到新分区
- ACCESS EXCLUSIVE 锁,操作期间相关表不可读写
- 大表操作耗时长,官方明确不建议
- 逻辑复制场景需确认发布配置,避免数据丢失
这些限制在 PG 20 有望逐步放宽。Alexander Korotkov 已经在规划自动复制 RLS、重算生成列等功能。但 PG 19 的策略是对的——宁可不让你做,也不让你做了之后数据出问题。
总结
- MERGE PARTITIONS 一条命令替代五步手工操作:DETACH+CREATE+INSERT+DROP 变成一条 SQL,数据原地搬家
- SPLIT PARTITION 反向操作:把一个分区按边界拆成多个,不用导出导入
- 不支持 Hash 分区:Range 和 List 分区表可用,Hash 不行
- Beta 期间修了三个大坑:生成列静默重算、RLS 策略丢失、逻辑复制数据丢失
- 修复策略是遇到不一致就报错:不自动处理,直接拒绝执行,完整功能留给 PG 20
- 操作前查四项:RLS 状态、生成列表达式、分区类型、逻辑复制发布配置
- 大表别用:MERGE 要移动全部数据,耗时很长,老办法分批处理更安全
- Andrew Dunstan 把它列进了 PG 19 主要特性清单:和 REPACK、pg_plan_advice 并列
转发给身边还在 DETACH+INSERT+DROP 合并分区的 DBA 同事,可能帮他省掉一晚上的活。




