PostgreSQL 19 的 PARTITION MERGE/SPLIT:这次锁对了

2026-05-14 29 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

PostgreSQL 的分区表功能逐年增强,但有一个操作一直让运维人员头疼——合并和拆分已有分区。早期版本曾尝试引入 MERGE PARTITIONSSPLIT PARTITION,却因为锁策略过于激进,在实际生产中几乎没法用。PostgreSQL 19 把这两条命令重新带回,核心改进就一个字:锁。新实现大幅缩小了阻塞范围,让分区重组终于可以在业务运行期间安全执行。

第一次尝试为何失败

PostgreSQL 早期版本(具体是 v14 开发周期中)曾提交过 MERGE PARTITIONSSPLIT PARTITION 的补丁。功能逻辑没问题:把多个分区合并成一个,或者把一个分区按新边界拆成多个。问题出在执行期间拿的锁——它对目标分区及其父表加了 ACCESS EXCLUSIVE 锁。

ACCESS EXCLUSIVE 是 PostgreSQL 最重的表级锁,会阻塞一切并发操作:读、写、甚至 SELECT 都得排队等操作完成。分区重组往往涉及大量数据移动,耗时可能数分钟到数小时。在这段时间里,相关表等于完全不可用。对于 7×24 运行的系统,这等于不可部署。

补丁最终被撤回,社区花了几年重新设计锁策略。

新版本的锁策略变化

PostgreSQL 19 的重新实现,关键改动是把重锁的持有时间压缩到最短:

  • 数据迁移阶段只拿 SHARE UPDATE EXCLUSIVE 或更轻的锁,允许并发读写继续进行。
  • 元数据切换瞬间(更新分区边界、绑定新分区到父表)才短暂提升到 ACCESS EXCLUSIVE,这个操作本身是毫秒级的字典更新,不涉及数据搬运。
  • 整体流程类似 ALTER TABLE ... SET LOGGEDREFRESH 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-012024-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-012024-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 把分区合并与拆分从"理论可用"推到了"生产可用"。锁策略的改进是决定性因素——长时间操作不再独占整张表,业务连续性有了保障。如果你的系统用范围分区管理历史数据滚动,这两条命令值得在升级后第一时间验证。


相关推荐