- 有时候图形客户端弹出一句“分区表正被其它程序独占访问”,并不是文件被占住,而是数据库内部的锁在排队。
这句话是我接手过好几个PostgreSQL生产环境之后最深的感触。很多同事第一次看到这个提示,第一反应是去检查文件权限、杀掉进程,甚至重启数据库,但真正的问题往往藏在锁等待里。PostgreSQL本身是支持分区表的,尤其PG 10之后引入了声明式分区,运维上一个分区就跟管理一张独立表一样方便。可一旦涉及到“删分区”“搬数据”“跨库迁移”,各种坑就开始往外冒。
这篇内容我不打算写成一版官方文档的复述,而是围绕分区表日常维护和迁移这两个核心场景,把我在真实环境里碰到过的问题、排查思路、最终落地的方案全部摊开讲。如果你正在用PostgreSQL管着一张不断增长的大表,或者正准备把一套业务从旧库迁到新库,这篇文章应该能帮你少走不少弯路。
1. 分区表维护的日常:加分区、归档删除与自动化的基础操作
1.1 为什么“分区表”是对运维最友好的数据组织方式之一
很多人第一次接触分区表会觉得麻烦,毕竟多了一层父表和子表的关系,写SQL的时候还得注意命名。但真正扛过千万级甚至亿级数据之后,你会感谢分区表带来的运维便利。
核心逻辑很简单:一张大表被拆成若干独立存储的分区,每个分区拥有自己的存储、索引和统计信息。查询的时候,优化器根据分区键直接裁剪掉无关分区,只扫必要的数据文件。维护的时候,你也不再需要面对“整表DELETE再VACUUM FULL”这种耗时间的操作,删掉一个分区就是一张表的DROP,秒级完成。
拿一个典型的订单表举例:
sql复制CREATE TABLE orders (
order_id bigint NOT NULL,
order_time timestamptz NOT NULL,
amount numeric(10,2)
) PARTITION BY RANGE (order_time);
CREATE TABLE orders_2024_01 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
PG 10之后的声明式分区会把父子关系、约束、默认分区都交给内核管理,不再需要手动写check约束和trigger。对比老式的“继承+触发器”方案,声明式分区在查询计划、锁处理和并发控制上都要干净得多,这也是我至今推荐团队首选声明式分区的原因。
1.2 日常加分区:手动SQL与自动扩展
分区表的维护频率通常是按月来算的。每个月月底加下一个月的分区,这种活看起来简单,但漏掉一次就会出问题:新数据没有对应分区,INSERT直接报错“no partition of relation found for row”。
手动加一个分区的SQL不复杂:
sql复制CREATE TABLE orders_2024_03 PARTITION OF orders
FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
CREATE INDEX ON orders_2024_03 (order_time);
问题在于,生产环境不会给你太多手动操作空间。最好是在应用发布流程里带上每月自动加分区的脚本,或者使用pg_partman这套扩展,它专门做分区生命周期管理。
pg_partman的基本思路是:给你一个定时任务,到点自动创建新分区、清理过期分区。安装后创建一套按月分区的配置大概是这样:
sql复制SELECT partman.create_parent(
p_parent_table => 'public.orders',
p_control => 'order_time',
p_interval => '1 month',
p_premake => 2
);
配合PgAgent还是外部cron,定时调用partman.run_maintenance()就能自动把下个月、下下个月的分区提前建好。用上这套之后,我基本没有再遇到过“今天1号发现0点数据写不进去”的尴尬。除非上游维护窗口临时取消,一般来说提前预建两个分区足够覆盖绝大多数情况。
另一个容易被忽略的点是默认分区。我强烈建议分区表保留一个default分区,用于路由边界外的数据。比如SQL里的时间异常,或者某个历史数据时间戳超出了预定范围,至少数据不会直接失败,而是落到default分区里暂存,后续再人工修正。代价是default分区扫起来会比较慢,但作为一个兜底机制,它远比整条链路报错强。
1.3 老分区归档:TRUNCATE与DROP的正确姿势
订单表越来越大之后,就得考虑历史分区怎么处理。最常见的需求是“只保留最近24个月数据”,那么超过24个月的分区需要删除。
这里的删除有两种方式:
DROP TABLE orders_2022_01;,直接连表结构带数据一起干掉,最彻底。TRUNCATE orders_2022_01;,只清数据,保留结构和索引。
大多数场景我会优先选DROP。原因在于TRUNCATE之后,分区依然存在,未来的INSERT如果有数据落入这个区间,还是会写进去
