分区表在PostgreSQL的生产使用中非常普遍,尤其是日志类、订单类这类按时间持续增长的大表。业务跑着跑着,新的月份或季度到了,就需要提前把未来的分区建好。如果操作不当,一条DDL可能把线上查询全部阻塞,甚至引发连接堆积。其实PostgreSQL提供了完善的不停机扩分区方案,核心是理解锁机制并控制好事务节奏。

一、先搞清楚分区DDL的锁行为
PostgreSQL中分区本身也是一张普通的表,新增分区的本质是把一张普通表挂到分区父表上。这个动作对应的语法有两种:一种是CREATE TABLE ... PARTITION OF直接创建,另一种是先建好普通表再用ALTER TABLE ... ATTACH PARTITION挂载。两者在锁行为上有明显差异。
直接用CREATE TABLE ... PARTITION OF创建新分区时,需要对父表加ACCESS EXCLUSIVE锁。这个锁级别最高,会阻塞父表上所有并发读写。如果此时有一个长事务正在查询父表,DDL就会等待;更危险的是,在DDL等待期间,后续所有访问该表的查询都会排在等待队列后面,形成连锁阻塞。很多线上事故就是这么发生的。
而ATTACH PARTITION的方式则友好得多。它同样需要ACCESS EXCLUSIVE锁,但持有时间极短——仅仅是把分区元数据挂到父表的目录上,事务提交后锁立即释放,通常在毫秒级完成。数据加载、索引创建这些耗时操作都可以在挂载之前的普通表上完成,完全不触碰父表。这就是不停机操作的理论基础。
二、不停机新增分区的完整操作步骤
推荐的标准流程是:先在分区体系之外建好普通表,完成数据准备,最后用极短的DDL事务挂载。假设有一个按月分区的订单表,现在要新增下个月的分区,步骤如下。
第一步,参照现有分区的结构创建普通表,注意要手动创建分区键上的约束,这样挂载时PostgreSQL可以跳过全表扫描验证:
-- 查看现有分区结构 \d+ orders_202501 -- 创建与分区结构一致的普通表 CREATE TABLE orders_202502 (LIKE orders INCLUDING DEFAULTS INCLUDING CONSTRAINTS); -- 关键:显式添加分区键约束,避免ATTACH时全表扫描 ALTER TABLE orders_202502 ADD CONSTRAINT orders_202502_check CHECK (created_at >= DATE '2025-02-01' AND created_at < DATE '2025-03-01');
第二步,在普通表上创建与父表一致的索引。父表上的每个索引都要在新表上对应创建,否则挂载后查询计划可能出现性能回退:
-- 参照父表索引逐一创建 CREATE INDEX orders_202502_user_id_idx ON orders_202502 (user_id); CREATE INDEX orders_202502_created_at_idx ON orders_202502 (created_at); -- 如有需要可提前ANALYZE收集统计信息 ANALYZE orders_202502;
第三步,用带锁超时的短事务执行挂载。设置lock_timeout是整套流程的安全阀,即使遇到长事务阻塞,也只会取消自己而不会堵塞别人:
BEGIN;
-- 设置锁等待超时,3秒拿不到锁就自动放弃
SET LOCAL lock_timeout = '3s';
ALTER TABLE orders ATTACH PARTITION orders_202502
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
COMMIT;
挂载成功后,PostgreSQL会自动把普通表上的索引挂为父表分区索引的子索引,前面创建的CHECK约束也会被识别为分区约束的一部分,不需要额外清理。
三、默认分区带来的数据迁移问题
如果分区表存在DEFAULT默认分区,那么未来时间范围的数据可能已经写进了默认分区。此时执行ATTACH会失败或者需要扫描默认分区,PostgreSQL会报错提示分区范围与默认分区存在重叠,要求先做数据迁移。
正确的处理顺序是:先从默认分区把属于新分区范围的数据迁移到新表中,再清空默认分区中这部分数据,最后才能ATTACH。直接DELETE加INSERT在数据量大时锁持有时间长,更稳妥的做法是利用事务保证一致性:
BEGIN;
-- 将默认分区中属于新分区的数据搬入新表
INSERT INTO orders_202502
SELECT * FROM orders_default
WHERE created_at >= DATE '2025-02-01' AND created_at < DATE '2025-03-01';
-- 删除默认分区中的这部分数据
DELETE FROM orders_default
WHERE created_at >= DATE '2025-02-01' AND created_at < DATE '2025-03-01';
COMMIT;
-- 数据清理完成后才能挂载
ALTER TABLE orders ATTACH PARTITION orders_202502
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
这段数据迁移是整个流程中耗时最长的部分,但它只涉及默认分区和新表两张普通表,不会锁父表。迁移期间新写入默认分区的数据可能造成冲突,所以如果业务允许,建议在低峰期执行,或者在迁移事务中使用行级锁小心处理并发写入。
四、常见坑点与排查方法
第一个坑是锁等待队列放大。即使ATTACH本身只持锁毫秒级,如果恰好有长事务持有父表的锁,DDL排队期间所有新查询都会被挡住。除了设置lock_timeout,还可以通过以下查询找到阻塞源头:
-- 查看谁阻塞了DDL
SELECT pid, wait_event_type, wait_event, query, state
FROM pg_stat_activity
WHERE wait_event_type = 'Lock';
-- 查看锁等待关系
SELECT blocked.pid AS blocked_pid, blocking.pid AS blocking_pid,
blocked.query AS blocked_query, blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid));
第二个坑是统计信息缺失。新挂载的分区如果没有ANALYZE,优化器可能给出错误的查询计划。挂载后务必对新分区执行一次ANALYZE,或者等待autovacuum自动收集,但对于立即承接流量的分区,手动执行更保险。
第三个坑是DETACH与ATTACH的配套使用。如果挂载后发现数据有问题需要回滚,可以用ALTER TABLE ... DETACH PARTITION把分区卸下来,同样只需短暂的ACCESS EXCLUSIVE锁。注意PG13及更早版本DETACH会等持有旧快照的事务结束,PG14开始支持DETACH PARTITION ... CONCURRENTLY,进一步降低了影响。
总结一下,不停机新增分区的三要素是:操作全部在独立普通表上完成、ATTACH用带lock_timeout的短事务执行、有默认分区时先迁移数据再挂载。掌握这些要点后,配合定时任务提前创建未来分区,就能彻底告别停机扩容的烦恼。
PostgreSQL分区表新增分区ATTACH PARTITION修改时间:2026-09-15 07:34:31