PostgreSQL 的分区表功能逐年增强,但有一个操作一直让运维人员头疼——合并和拆分已有分区。早期版本曾尝试引入 MERGE PARTITIONS 和 SPLIT PARTITION,却因为锁策略过于激进,在实际生产中几乎没法用。PostgreSQL 19 把这两条命令重新带回,核心改进就一个字:锁。新实现大幅缩小了阻塞范围,让分区重组终于可以在业务运行期间安全执行。
第一次尝试为何失败
PostgreSQL 早期版本(具体是 v14 开发周期中)曾提交过 MERGE PARTITIONS 与 SPLIT PARTITION 的补丁。功能逻辑没问题:把多个分区合并成一个,或者把一个分区按新边界拆成多个。问题出在执行期间拿的锁——它对目标分区及其父表加了 ACCESS EXCLUSIVE 锁。
ACCESS EXCLUSIVE 是 PostgreSQL 最重的表级锁,会阻塞一切并发操作:读、写、甚至 SELECT 都得排队等操作完成。分区重组往往涉及大量数据移动,耗时可能数分钟到数小时。在这段时间里,相关表等于完全不可用。对于 7×24 运行的系统,这等于不可部署。
补丁最终被撤回,社区花了几年重新设计锁策略。
新版本的锁策略变化
PostgreSQL 19 的重新实现,关键改动是把重锁的持有时间压缩到最短:
- 数据迁移阶段只拿
SHARE UPDATE EXCLUSIVE或更轻的锁,允许并发读写继续进行。 - 元数据切换瞬间(更新分区边界、绑定新分区到父表)才短暂提升到
ACCESS EXCLUSIVE,这个操作本身是毫秒级的字典更新,不涉及数据搬运。 - 整体流程类似
ALTER TABLE ... SET LOGGED或REFRESH MATERIALIZED VIEW CONCURRENTLY的思路:长时间干活时轻锁,只在最后切换状态时短暂重锁。
这意味着合并一个几十 GB 的分区,数据迁移期间业务查询和写入几乎不受影响,只在最后一瞬间有一个极短的阻塞窗口。
实际操作:MERGE 与 SPLIT
下面用一组可运行的 SQL 演示两条命令的基本用法。先建一张按范围分区的订单表:
-- 创建分区父表
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY,
order_date DATE NOT NULL,
amount NUMERIC(10,2)
) PARTITION BY RANGE (order_date);
-- 创建三个按季度划分的分区
CREATE TABLE orders_q1 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE orders_q2 PARTITION OF orders
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
CREATE TABLE orders_q3 PARTITION OF orders
FOR VALUES FROM ('2024-07-01') TO ('2024-10-01');
-- 插入一些测试数据
INSERT INTO orders (order_date, amount) VALUES
('2024-02-15', 120.00),
('2024-05-10', 340.50),
('2024-08-22', 560.00);
合并分区:把 Q1 和 Q2 合成一个上半年分区
-- MERGE:将两个相邻分区合并为一个
ALTER TABLE orders
MERGE PARTITIONS orders_q1, orders_q2
INTO orders_h1;
-- 检查结果:现在只有两个分区
SELECT partition_name, boundary_start, boundary_end
FROM pg_partitions
WHERE parent_table = 'public.orders';
合并后 orders_h1 的范围自动扩展为 2024-01-01 到 2024-07-01,覆盖原来两个分区的完整区间。原有数据原地保留,不需要物理搬运(因为底层实际是同一个表对象被重新绑定)。
拆分分区:把 Q3 拏成 Q3 和 Q4
-- SPLIT:在指定边界处将一个分区拆成两个
ALTER TABLE orders
SPLIT PARTITION orders_q3
AT ('2024-07-01')
INTO (orders_q3_new, orders_q4);
-- 验证拆分后的分区边界
SELECT partition_name, boundary_start, boundary_end
FROM pg_partitions
WHERE parent_table = 'public.orders';
AT 指定的是新分区的起始边界——orders_q3_new 覆盖 2024-07-01 到 2024-10-01 之前的部分,orders_q4 覆盖剩余部分。落在新边界两侧的行会被自动迁移到对应分区。
注意:以上语法基于 PostgreSQL 19 当前开发分支的提案形式。正式发布时关键字或子句细节可能有微调,部署前务必对照官方文档确认最终语法。
执行期间的并发观察
为了直观感受锁的变化,可以在一个会话执行 MERGE/SPLIT,同时在另一个会话观察锁类型:
-- 会话 A:执行合并(假设数据量较大,耗时明显)
ALTER TABLE orders MERGE PARTITIONS orders_q1, orders_q2 INTO orders_h1;
-- 会话 B:同时查看持有的锁
SELECT locktype, relation::regclass, mode, granted
FROM pg_locks
WHERE pid = <会话A的PID>
AND locktype = 'relation';
在数据迁移阶段,你应该看到 ShareUpdateExclusiveLock 而不是 AccessExclusiveLock。这意味着会话 B 可以同时执行:
-- 会话 C:合并期间并发查询——不会被阻塞
SELECT count(*) FROM orders WHERE order_date < '2024-06-01';
只有最后元数据切换的极短窗口,并发查询才会等待,通常在毫秒级完成。
部署前的检查清单
| 检查项 | 说明 |
|---|---|
| 版本确认 | MERGE/SPLIT 在 PostgreSQL 19 正式版中可用,早期版本不支持 |
| 分区连续性 | MERGE 要求目标分区在边界上相邻,不能跳区间合并 |
| 外键与索引 | 合并/拆分后检查子表上的索引是否自动继承,外键约束是否需要重建 |
| 事务大小 | 大分区拆分会产生大量行迁移,监控 pg_stat_progress_partition(如有)或 WAL 流量 |
| 维护窗口 | 虽然锁变轻了,元数据切换瞬间仍有 ACCESS EXCLUSIVE,低峰期操作更稳妥 |
| 备份验证 | 操作前确认最近一次备份可恢复,分区重组是不可逆的结构变更 |
PostgreSQL 19 把分区合并与拆分从"理论可用"推到了"生产可用"。锁策略的改进是决定性因素——长时间操作不再独占整张表,业务连续性有了保障。如果你的系统用范围分区管理历史数据滚动,这两条命令值得在升级后第一时间验证。