暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

合并分区还在 DETACH+INSERT+DROP?PG 19 一条 SQL 搞定

FortuneXu 1天前
5

上周有个 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 的执行过程:

  1. 以分区表本身为模板,创建新分区 orders_2026q1
  2. 继承父表的访问方法、持久化类型、表空间
  3. 把三个源分区的数据搬进新分区
  4. 数据迁移完成后,创建索引和 identity 列
  5. 以 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。

更麻烦的是订阅端的传播链路:

  1. 发布端执行 MERGE
  2. 订阅端还没来得及处理,发布端对已合并行做了 UPDATE
  3. 订阅端恢复时发现新分区不在订阅中,丢弃了这个 UPDATE
  4. 用户执行 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 是第一版,限制不少:

  1. 不支持 Hash 分区,未来也不太可能支持(哈希空间变了数据路由全乱)
  2. 只能操作叶子分区,子分区化的分区不能直接 MERGE/SPLIT
  3. 有 RLS 策略的分区直接拒绝执行
  4. 生成列表达式必须与父表一致,否则拒绝
  5. 扩展统计和安全策略不会被复制到新分区
  6. ACCESS EXCLUSIVE 锁,操作期间相关表不可读写
  7. 大表操作耗时长,官方明确不建议
  8. 逻辑复制场景需确认发布配置,避免数据丢失

这些限制在 PG 20 有望逐步放宽。Alexander Korotkov 已经在规划自动复制 RLS、重算生成列等功能。但 PG 19 的策略是对的——宁可不让你做,也不让你做了之后数据出问题。


总结

  1. MERGE PARTITIONS 一条命令替代五步手工操作:DETACH+CREATE+INSERT+DROP 变成一条 SQL,数据原地搬家
  2. SPLIT PARTITION 反向操作:把一个分区按边界拆成多个,不用导出导入
  3. 不支持 Hash 分区:Range 和 List 分区表可用,Hash 不行
  4. Beta 期间修了三个大坑:生成列静默重算、RLS 策略丢失、逻辑复制数据丢失
  5. 修复策略是遇到不一致就报错:不自动处理,直接拒绝执行,完整功能留给 PG 20
  6. 操作前查四项:RLS 状态、生成列表达式、分区类型、逻辑复制发布配置
  7. 大表别用:MERGE 要移动全部数据,耗时很长,老办法分批处理更安全
  8. Andrew Dunstan 把它列进了 PG 19 主要特性清单:和 REPACK、pg_plan_advice 并列

转发给身边还在 DETACH+INSERT+DROP 合并分区的 DBA 同事,可能帮他省掉一晚上的活。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

文章被以下合辑收录

评论