分区维护(Partition Maintenance)
分区表最核心的运维价值,是让「删除海量历史数据」从一次漫长的批量 DELETE 变成一次近乎瞬时的元数据操作。官方文档把这一点列为分区的主要收益之一:DROP TABLE 单个分区或 ALTER TABLE DETACH PARTITION 远快于批量操作,并且完全避免了批量 DELETE 带来的 VACUUM 开销。
本页讲清楚四条清理路径(DROP / DETACH / TRUNCATE / DELETE)的代价差异、DETACH ... CONCURRENTLY 的并发语义与限制、FINALIZE 的恢复用法,以及分区轮转的自动化与治理方法。
基础概念
为什么分区能做「瞬时删除」
普通表的 DELETE FROM t WHERE created_at < '2025-01-01' 必须逐行定位、标记删除、写 WAL,之后还要靠 VACUUM 回收死元组。删一百亿行,就是一百亿次行级操作。
分区表把「一批行」变成了「一个文件集合」。删除整月数据只要把承载它的分区摘掉:
DELETE(行级操作) DROP/DETACH(元数据操作)
├─ 逐行加锁、标记 xmax ├─ 改写 pg_class / pg_inherits
├─ 每行写 WAL ├─ 删除/解绑物理文件
├─ 索引逐项维护 ├─ 索引随文件一并消失
└─ 遗留死元组 → 必须 VACUUM └─ 无死元组 → 无 VACUUM 开销四条清理路径的定位
| 操作 | 数据是否保留 | 父表锁(PG 18) | 典型用途 |
|---|---|---|---|
DROP TABLE partition | 丢弃 | ACCESS EXCLUSIVE | 确认不再需要的旧分区,最快 |
DETACH PARTITION | 保留(变独立表) | ACCESS EXCLUSIVE | 先摘链、导数据、再删除 |
DETACH PARTITION CONCURRENTLY | 保留 | SHARE UPDATE EXCLUSIVE(PG 14+) | 在线归档,不阻塞父表读写 |
TRUNCATE partition | 丢弃行、保留表结构 | 分区自身的 ACCESS EXCLUSIVE | 清空当期数据(如日志重跑) |
DELETE FROM partition | 丢弃行 | 行级锁 | 只删部分行,无法整分区清理 |
关键结论:只要能按分区粒度删,就绝不要用 DELETE。 DELETE 是唯一会留下 VACUUM 债务的路径。
DETACH 的两个阶段
DETACH PARTITION CONCURRENTLY(PostgreSQL 14+)内部用两个事务实现「不阻塞」:
事务 1(SHARE UPDATE EXCLUSIVE 锁)
├─ 在 pg_inherits.inhdetachpending 上把该分区标为「正在摘除」
└─ 提交后,新查询的规划期不再把它当作分区;已执行中的查询仍可安全访问
事务 2(独立事务)
├─ 真正解除继承关系,刷新缓存
└─ 完成后分区成为普通表正因为它跨两个事务,不能在事务块内执行,也不能放进存储过程。如果第二个事务因崩溃或取消而没跑完,分区会停在「pending detach」状态,需要用 FINALIZE 补齐。
pending detach 与 FINALIZE
pg_inherits.inhdetachpending 列(bool)标记该分区正在被摘除。官方对 FINALIZE 的定义是:完成一次此前被取消或中断的 DETACH CONCURRENTLY,并且同一时刻一张分区表最多只能有一个分区处于 pending detach 状态。
-- 查看是否有分区停在 pending detach
SELECT c.relname AS partition, i.inhdetachpending
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
WHERE i.inhparent = 'orders'::regclass
AND i.inhdetachpending;这是分区维护中唯一需要「补救」的异常状态,维护脚本应当主动巡检它。
锁级别的整体认识
| 语句 | 父表锁 | 说明 |
|---|---|---|
DROP TABLE partition | ACCESS EXCLUSIVE | 阻塞父表所有读写 |
DETACH PARTITION | ACCESS EXCLUSIVE | 阻塞父表所有读写 |
DETACH PARTITION CONCURRENTLY | SHARE UPDATE EXCLUSIVE | 不阻塞读写,但限制多 |
DETACH PARTITION ... FINALIZE | SHARE UPDATE EXCLUSIVE | 完成中断的并发摘除 |
TRUNCATE partition | 分区自身 ACCESS EXCLUSIVE | 不经父表,但会打断使用该分区的查询 |
注意
DROP TABLE 与 DETACH 都取父表 ACCESS EXCLUSIVE 锁,必须等所有引用该父表的查询结束才能拿到。长事务、长查询会让这一步排队,进而阻塞它后面排队的全部查询。维护窗口要避开长事务。
语法与用法
-- 直接删除分区(连带数据)
DROP TABLE partition_name;
-- 摘下分区,保留为独立表
ALTER TABLE parent DETACH PARTITION partition_name
ALTER TABLE parent DETACH PARTITION partition_name CONCURRENTLY
ALTER TABLE parent DETACH PARTITION partition_name FINALIZE
-- 只清空分区里的行,保留表
TRUNCATE [ ONLY ] partition_name
-- 删除分区后重建(轮转常用组合)
ALTER TABLE parent DETACH PARTITION old_partition;
DROP TABLE old_partition;最简可运行示例:
-- 先用一个 3 分区的月度表做演示
CREATE TABLE orders (
id bigint NOT NULL,
user_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
created_at date NOT NULL
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2026_01 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');CREATE TABLE
CREATE TABLE
CREATE TABLE-- 灌入一点数据,便于观察行数与大小
INSERT INTO orders VALUES (1, 1001, 10.00, '2026-01-05');
INSERT INTO orders VALUES (2, 1002, 20.00, '2026-01-20');
INSERT INTO orders VALUES (3, 1003, 30.00, '2026-02-10');INSERT 0 1
INSERT 0 1
INSERT 0 1方式一:直接 DROP(数据不需要了)
DROP TABLE orders_2026_01;DROP TABLE方式二:先 DETACH 再处理(数据还要归档)
ALTER TABLE orders DETACH PARTITION orders_2026_02;
-- 此时 orders_2026_02 仍是普通表,数据完好
SELECT count(*) FROM orders_2026_02;ALTER TABLE
count
-------
1
(1 row)方式三:并发摘除(PostgreSQL 14+,在线归档);方式四:清空分区但不删表
ALTER TABLE orders DETACH PARTITION orders_2026_02 CONCURRENTLY; -- 不阻塞父表读写
TRUNCATE orders_2026_02; -- 只清行、保留表ALTER TABLE
TRUNCATE TABLE维护后确认结构
SELECT relid::regclass AS partition, level, isleaf
FROM pg_partition_tree('orders');
SELECT relid::regclass AS ancestor
FROM pg_partition_ancestors('orders_2026_02'); partition | level | isleaf
---------------+-------+--------
orders | 0 | f
orders_2026_02| 1 | t
(2 rows)
ancestor
-------------
orders_2026_02
orders
(2 rows)详细说明
DROP TABLE:最快的数据清理
对分区执行 DROP TABLE 只涉及元数据改写与文件删除,不逐行处理。官方原文强调它「可以非常快地删除数百万条记录,因为它不必逐行删除」。
-- 删除 2025 全年 12 个分区,几乎是同一个瞬间完成
DROP TABLE orders_2025_01, orders_2025_02, orders_2025_03, orders_2025_04,
orders_2025_05, orders_2025_06, orders_2025_07, orders_2025_08,
orders_2025_09, orders_2025_10, orders_2025_11, orders_2025_12;代价在于锁:该命令需要对父表取 ACCESS EXCLUSIVE 锁,因为要改写分区表的分区列表并让所有依赖缓存失效。若父表上还有未结束的长事务,它会一直排队,并连带阻塞后面排队的查询。
DETACH:先摘链再处理
DETACH 把分区从父表上摘下来,它继续作为独立普通表存在。官方给的典型用法是在删除前留一个「操作窗口」:用 COPY、pg_dump 备份,或把明细聚合成更小的汇总格式。
ALTER TABLE orders DETACH PARTITION orders_2026_01;
-- 归档:导出成 CSV
COPY orders_2026_01 TO '/data/archive/orders_2026_01.csv' WITH (FORMAT csv, HEADER);
-- 归档:聚合成月度汇总后再删
CREATE TABLE orders_monthly_summary (
month date PRIMARY KEY,
total numeric(18,2),
cnt bigint
);
INSERT INTO orders_monthly_summary
SELECT date_trunc('month', created_at)::date, sum(amount), count(*)
FROM orders_2026_01
GROUP BY 1;
DROP TABLE orders_2026_01;DETACH 完成后,被摘下的表会保留一个与分区边界等价的 CHECK 约束。这既是数据完整性的痕迹,也让日后重新 ATTACH 时可以直接跳过校验扫描。
SELECT conname, pg_get_constraintdef(oid) AS def
FROM pg_constraint
WHERE conrelid = 'orders_2026_01'::regclass AND contype = 'c'; conname | def
------------------+-------------------------------------------------------
orders_2026_01_created_at_check | CHECK (created_at >= '2026-01-01' AND created_at < '2026-02-01')
(1 row)DETACH PARTITION CONCURRENTLY(PostgreSQL 14+)
这是分区在线运维的主力语句。它把父表锁降到 SHARE UPDATE EXCLUSIVE,不阻塞父表上的读写。代价是三条硬性限制:
| 限制 | 说明 |
|---|---|
| 不能在事务块内执行 | SQLSTATE 25001,BEGIN/存储过程里都不可用 |
父表不能有 DEFAULT 分区 | 核内直接拒绝 |
| 同一父表同时只能有一个 pending detach | 多个并发摘除需要串行 |
-- 正确:作为顶层单条命令执行
ALTER TABLE orders DETACH PARTITION orders_2025_12 CONCURRENTLY;ALTER TABLE在事务块内执行会被明确拒绝:
ERROR: ALTER TABLE ... DETACH CONCURRENTLY cannot run inside a transaction block父表存在 DEFAULT 分区时同样被拒绝:
ERROR: cannot detach partitions concurrently when a default partition exists因此「想用并发摘除」与「保留 DEFAULT 分区兜底」在设计上互斥,需要提前取舍。
DETACH ... FINALIZE 与 pending detach 的恢复
如果 CONCURRENTLY 的第二个事务被取消、连接中断或实例崩溃,分区会停在 pending detach 状态:它对新查询已不可见,但继承关系还没真正解除。
-- 巡检:找出停在 pending detach 的分区
SELECT c.relname AS partition,
pg_size_pretty(pg_total_relation_size(c.oid)) AS size
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
WHERE i.inhparent = 'orders'::regclass
AND i.inhdetachpending; partition | size
---------------+--------
orders_2025_12| 8192 kB
(1 row)恢复动作就是补一条 FINALIZE:
ALTER TABLE orders DETACH PARTITION orders_2025_12 FINALIZE;ALTER TABLEFINALIZE 也只需父表 SHARE UPDATE EXCLUSIVE 锁,可以放心在业务时段补执行。若巡检结果为空,说明没有未完成的摘除。
TRUNCATE:清空单个分区
TRUNCATE 丢掉分区内的全部行但保留表结构,比 DELETE 快,也不需要 VACUUM。它取分区自身的 ACCESS EXCLUSIVE 锁,不经父表,因此不会阻塞父表上的其他分区访问,但会打断正在使用该分区的查询。它还是事务性的,可以回滚。
-- 只清空一个分区;在事务内可回滚
BEGIN;
TRUNCATE orders_2026_02;
ROLLBACK;
SELECT count(*) FROM orders_2026_02; -- 行数不变BEGIN
TRUNCATE TABLE
ROLLBACK
count
-------
1
(1 row)TRUNCATE 加到父表上会级联清空所有分区,这是常见的误操作来源:
-- 危险!这会清空 orders 的全部数据
TRUNCATE orders;TRUNCATE TABLE对比 DELETE / TRUNCATE / DETACH / DROP
同一批数据,四条路径的成本天差地别。以「删除一整月、约 2 亿行」为例:
| 维度 | DELETE | TRUNCATE | DETACH | DROP |
|---|---|---|---|---|
| 行级写 WAL | 是 | 否 | 否 | 否 |
| 索引维护 | 逐项 | 整文件重建 | 随表脱离 | 随表消失 |
事后 VACUUM | 必须 | 不需要 | 不需要 | 不需要 |
| 数据可保留 | 否 | 否 | 是 | 否 |
| 父表锁 | 行级 | 无(锁分区) | ACCESS EXCLUSIVE / SHARE UPDATE EXCLUSIVE | ACCESS EXCLUSIVE |
| 典型耗时 | 小时级 | 秒级 | 毫秒级 | 毫秒级 |
-- 不推荐:整月清理却用 DELETE,留下巨量死元组
DELETE FROM orders WHERE created_at >= '2026-02-01' AND created_at < '2026-03-01';DELETE 200000000
Time: 1834521.203 ms-- 推荐:整月清理直接摘分区
ALTER TABLE orders DETACH PARTITION orders_2026_02;ALTER TABLE
Time: 8.412 ms需要强调:只有当条件恰好等于某个分区的边界时才能整分区摘除。若条件跨越多个分区,可先拆成多个整分区操作,只对残余的边界分区用 DELETE。
索引、约束与触发器的随动
DETACH 与 DROP 会自动处理从父表传播下来的对象,无需手工清理:
-- 父表上的索引会传播到每个分区
CREATE INDEX orders_created_idx ON orders (created_at);CREATE INDEX-- 摘除后,分区的索引跟着脱离,但依然存在(它仍是独立表的索引)
ALTER TABLE orders DETACH PARTITION orders_2026_02;
SELECT indexrelid::regclass AS idx
FROM pg_index
WHERE indrelid = 'orders_2026_02'::regclass; idx
-----------------------------------
orders_2026_02_created_at_idx
(1 row)若分区本身还有二级子分区,DETACH 会把整棵子树一起摘下——脱离后的根成为一棵独立的分区树,而不是被拆散。
-- 查看子树规模,确认摘除影响范围
SELECT count(*) AS leaf_count
FROM pg_partition_tree('orders_2026_02')
WHERE isleaf; leaf_count
------------
1
(1 row)用 pg_partman 自动化轮转
手工建分区、手工摘除在几十个分区时还能应付,上百个就必须交给工具。pg_partman 是社区事实标准,它在 part_config 表中登记每个父表的轮转策略。
-- 在 part_config 中登记父表:按月分区、提前建 4 个月、只保留 12 个月
SELECT partman.create_parent(
p_parent_table => 'public.orders',
p_control => 'created_at',
p_interval => '1 month',
p_premake => 4
);
UPDATE partman.part_config
SET retention = '12 months',
retention_keep_table = false, -- 过期即 DROP,而不是只 DETACH
retention_keep_index = false
WHERE parent_table = 'public.orders'; create_parent
------------------
public.orders
(1 row)
UPDATE 1之后由定时任务调用 run_maintenance(),它一次完成「预建未来分区 + 清理过期分区」:
CALL partman.run_maintenance(p_analyze => false);CALL-- 查看登记与运行状态
SELECT parent_table, partition_interval, premake, retention, retention_keep_table
FROM partman.part_config; parent_table | partition_interval | premake | retention | retention_keep_table
---------------+--------------------+---------+------------+----------------------
public.orders | 1 mon | 4 | 12 months | f
(1 row)分区数量治理
分区不是越多越好。每个分区在规划期都要参与裁剪判断、每次 DDL 都要遍历整棵分区树,分区数上千后规划开销与锁范围都会显著恶化。
-- 统计每个父表的分区数与总大小
SELECT parent.relname AS parent_table,
count(*) AS partition_count,
pg_size_pretty(sum(pg_total_relation_size(child.oid))) AS total_size
FROM pg_inherits i
JOIN pg_class parent ON parent.oid = i.inhparent
JOIN pg_class child ON child.oid = i.inhrelid
GROUP BY parent.relname
ORDER BY count(*) DESC; parent_table | partition_count | total_size
--------------+-----------------+------------
orders | 240 | 412 GB
(1 row)按月分区攒满 20 年就是 240 个分区,此时应考虑合并粒度(月 → 季)或引入多级分区,把一级分区数控制在几十的量级。同时要能识别「空分区」——对分区执行 SELECT count(*) 复核真实行数,确认后再合并或删除,避免误删刚预建、尚未写入的未来分区。
维护脚本:批量生成与巡检
日常维护应固化成两类脚本:批量生成 DDL 与结构巡检。前者用 format() 的 %I(标识符)与 %L(字面量)拼接,后者用系统目录函数核对结果。
-- 批量预建未来 6 个月分区,可重复执行
DO $$
DECLARE
m date := date_trunc('month', CURRENT_DATE)::date;
i int;
pname text;
BEGIN
FOR i IN 0..5 LOOP
pname := format('orders_%s', to_char(m + (i || ' month')::interval, 'YYYY_MM'));
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF orders
FOR VALUES FROM (%L) TO (%L)',
pname,
m + (i || ' month')::interval,
m + ((i + 1) || ' month')::interval
);
END LOOP;
END $$;DO-- 巡检:各分区的边界、行数与大小
SELECT c.relname AS partition,
pg_get_expr(c.relpartbound, c.oid) AS bound,
c.reltuples::bigint AS est_rows,
pg_size_pretty(pg_total_relation_size(c.oid)) AS size
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
WHERE i.inhparent = 'orders'::regclass
AND NOT i.inhdetachpending
ORDER BY c.relname; partition | bound | est_rows | size
----------------+---------------------------------------------+----------+---------
orders_2026_01 | FOR VALUES FROM ('2026-01-01') TO ('2026-02-01') | 1200000 | 96 MB
orders_2026_02 | FOR VALUES FROM ('2026-02-01') TO ('2026-03-01') | 1310000 | 104 MB
(2 rows)巡检脚本还有一个必查项:DEFAULT 分区是否异常增长。它变大通常意味着某个分区被漏建。
SELECT pg_size_pretty(pg_total_relation_size('orders_default')) AS default_size,
(SELECT count(*) FROM orders_default) AS default_rows; default_size | default_rows
--------------+--------------
142 MB | 812345
(1 row)把上面的动作串起来,一个典型的时间序列分区表每日/每月维护节奏是:
T-4 月 预建 T+4 的分区(CREATE TABLE ... PARTITION OF)
T-1 月 确认下月分区存在(巡检脚本)
T+0 新月份数据自动路由到新分区
T+12月 DETACH PARTITION CONCURRENTLY 摘除最旧分区
T+12月 归档该分区数据(COPY / 汇总)
T+12月 DROP TABLE 该分区
常驻 巡检 pending detach、DEFAULT 分区体积、分区总数# 用 cron 驱动的每日维护入口
0 3 * * * psql -d appdb -c "CALL partman.run_maintenance(p_analyze => false);"
15 3 * * * psql -d appdb -c "SELECT partman.check_default();"实战示例
第 1 步:建立带索引的月度分区表
CREATE TABLE orders (
id bigint NOT NULL,
user_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
created_at date NOT NULL
) PARTITION BY RANGE (created_at);
CREATE INDEX orders_created_idx ON orders (created_at);CREATE TABLE
CREATE INDEX第 2 步:预建 2026 年 1—3 月分区
CREATE TABLE orders_2026_01 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE orders_2026_03 PARTITION OF orders
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');CREATE TABLE
CREATE TABLE
CREATE TABLE第 3 步:写入数据并确认路由
INSERT INTO orders
SELECT g, 1000 + g % 100, (g % 500)::numeric, DATE '2026-01-01' + (g % 90)
FROM generate_series(1, 100000) AS g;INSERT 0 100000SELECT tableoid::regclass AS partition, count(*)
FROM orders GROUP BY 1 ORDER BY 1; partition | count
----------------+-------
orders_2026_01 | 33333
orders_2026_02 | 30333
orders_2026_03 | 36334
(3 rows)第 4 步:在线摘除最旧分区(并发)
ALTER TABLE orders DETACH PARTITION orders_2026_01 CONCURRENTLY;ALTER TABLE此语句只需父表 SHARE UPDATE EXCLUSIVE 锁,期间父表读写不受影响。
第 5 步:确认 pending detach 状态并巡检
SELECT c.relname AS partition, i.inhdetachpending
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
WHERE i.inhparent = 'orders'::regclass; partition | inhdetachpending
----------------+------------------
orders_2026_02 | f
orders_2026_03 | f
(2 rows)orders_2026_01 已不在列表中,说明摘除彻底完成。
第 6 步:归档并删除
# 摘除后的表是普通表,可直接逻辑导出
pg_dump -d appdb -t orders_2026_01 -Fc -f /data/archive/orders_2026_01.dumpDROP TABLE orders_2026_01;DROP TABLE第 7 步:清理后确认剩余分区仍可用
SELECT count(*) FROM orders WHERE created_at >= '2026-02-01' AND created_at < '2026-03-01'; count
-------
30333
(1 row)EXPLAIN (COSTS OFF)
SELECT count(*) FROM orders WHERE created_at >= '2026-03-01' AND created_at < '2026-04-01'; QUERY PLAN
----------------------------------------------
Aggregate
-> Seq Scan on orders_2026_03 orders
(2 rows)裁剪到单分区,说明删除旧分区没有破坏分区元数据。
注意事项
DROP TABLE与DETACH都需要父表ACCESS EXCLUSIVE锁。 官方文档在给出DROP TABLE measurement_y2006m02示例时明确提示「该命令需要在父表上取得ACCESS EXCLUSIVE锁」。长事务会让这一步排队并连带阻塞后续查询。sql-- 错误!在业务高峰直接执行,可能因等锁阻塞整张父表 DROP TABLE orders_2026_02; -- 正确:先看有没有长事务,或改用 DETACH CONCURRENTLY SELECT pid, now() - xact_start AS xact_age, query FROM pg_stat_activity WHERE state <> 'idle' AND xact_start IS NOT NULL ORDER BY xact_age DESC LIMIT 5;DETACH PARTITION CONCURRENTLY不能在事务块内执行。 无论显式BEGIN还是存储过程体内都不行。sql-- 错误!SQLSTATE 25001 BEGIN; ALTER TABLE orders DETACH PARTITION orders_2026_01 CONCURRENTLY; COMMIT; -- 正确:作为顶层命令单独执行 ALTER TABLE orders DETACH PARTITION orders_2026_01 CONCURRENTLY;父表存在
DEFAULT分区时不能用CONCURRENTLY。 这是内核层面的限制,不是配置问题。textERROR: cannot detach partitions concurrently when a default partition exists同一父表同时只能有一个分区处于 pending detach。 想批量并发摘除多个分区只能串行执行。
sql-- 不推荐:连续两条 CONCURRENTLY,第二条可能撞上未完成的 pending detach ALTER TABLE orders DETACH PARTITION orders_2026_01 CONCURRENTLY; ALTER TABLE orders DETACH PARTITION orders_2026_02 CONCURRENTLY; -- 正确:先 FINALIZE 补齐,再摘下一个 ALTER TABLE orders DETACH PARTITION orders_2026_01 FINALIZE;被中断的
CONCURRENTLY会留下 pending detach 状态。 必须用FINALIZE收尾,否则该分区既不是正常分区、也还没成为独立表。sql-- 巡检出 pending 分区后修复 SELECT c.relname FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid WHERE i.inhparent = 'orders'::regclass AND i.inhdetachpending; ALTER TABLE orders DETACH PARTITION orders_2026_01 FINALIZE;TRUNCATE父表会清空所有分区。 这不是等价于「清空一个表」,而是级联操作。sql-- 危险!清空 orders 全部分区 TRUNCATE orders; -- 正确:只清一个分区 TRUNCATE orders_2026_02;不要用
DELETE做整分区清理。 它会留下大量死元组,必须VACUUM,且全程写 WAL。sql-- 不推荐 DELETE FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01'; -- 推荐(数据可丢) DROP TABLE orders_2026_01; -- 推荐(数据要留) ALTER TABLE orders DETACH PARTITION orders_2026_01 CONCURRENTLY;空分区不能只看
reltuples。 统计信息未更新时会显示-1或旧值,误删预建分区会导致写入报no partition of relation ... found for row;应先用SELECT count(*) FROM orders_2027_11;复核真实行数。DETACH不会自动删除数据,DROP才会。 「摘下来忘了删」会造成空间持续增长,归档流程要有闭环。sql-- 找出已摘除但未删除的遗留表 SELECT c.relname, pg_size_pretty(pg_total_relation_size(c.oid)) AS size FROM pg_class c LEFT JOIN pg_inherits i ON i.inhrelid = c.oid WHERE c.relname LIKE 'orders\_%' AND i.inhrelid IS NULL ORDER BY pg_total_relation_size(c.oid) DESC;DETACH CONCURRENTLY期间查询可能瞬时失败。 早期版本存在「规划后、执行前分区被摘除」导致的偶发错误,PostgreSQL 14.13 起已修复裁剪准备逻辑;仍建议把该语句放在业务低峰期执行。
最佳实践
整分区清理一律走
DROP或DETACH,永远不用DELETE。 官方把「避免批量DELETE的VACUUM开销」列为分区的核心收益,应作为团队规范。sql-- 数据可丢 DROP TABLE orders_2026_01;需要归档的数据用
DETACH PARTITION CONCURRENTLY(PostgreSQL 14+)。 把父表锁从ACCESS EXCLUSIVE降到SHARE UPDATE EXCLUSIVE,在线完成下线。sqlALTER TABLE orders DETACH PARTITION orders_2026_01 CONCURRENTLY;维护脚本必须先巡检 pending detach,再执行新摘除。 保证同一父表不会有第二个摘除排队。
sqlSELECT c.relname FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid WHERE i.inhparent = 'orders'::regclass AND i.inhdetachpending;归档与删除要拆成两步,中间留观察期。
DETACH后先COPY/pg_dump,确认归档可用再DROP。bashpg_dump -d appdb -t orders_2026_01 -Fc -f /data/archive/orders_2026_01.dump预建分区交给
pg_partman或调度脚本,不要靠人工记忆。p_premake保证始终有未来若干期分区存在。sqlSELECT partman.create_parent( p_parent_table => 'public.orders', p_control => 'created_at', p_interval => '1 month', p_premake => 4 );保留策略明确区分
DETACH与DROP。retention_keep_table = false表示到期直接DROP,为true则只摘链保留。sqlUPDATE partman.part_config SET retention = '12 months', retention_keep_table = false WHERE parent_table = 'public.orders';把结构巡检固化为一条 SQL。 分区总数、
DEFAULT分区体积、pending detach 三项合并巡检,异常即为漏建或未收尾。sqlSELECT (SELECT count(*) FROM pg_inherits WHERE inhparent = 'orders'::regclass) AS partitions, pg_size_pretty(pg_total_relation_size('orders_default')) AS default_size, (SELECT count(*) FROM pg_inherits WHERE inhparent = 'orders'::regclass AND inhdetachpending) AS pending_detach;批量生成分区 DDL 必须用
format()的%I/%L并加IF NOT EXISTS。 保证脚本可重复执行、可审计、无注入风险。sqlEXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF orders FOR VALUES FROM (%L) TO (%L)', pname, lo, hi);所有维护语句写成可回滚或可重试的形式。
TRUNCATE可回滚(BEGIN; TRUNCATE orders_2026_02;发现异常即ROLLBACK;);DETACH+DROP应分步执行,便于中途叫停。用
pg_partition_tree与pg_partition_ancestors做结构断言。 每次维护后用它们核对树形是否符合预期(PostgreSQL 12+)。sqlSELECT relid::regclass, level, isleaf FROM pg_partition_tree('orders') ORDER BY level, relid;
下一节
下一节:分区索引策略(Partitioned Indexes)——理解分区表上的索引如何传播、唯一约束为何必须包含分区键,以及如何无阻塞地为大分区表建索引。
参考链接
- PostgreSQL 18 官方文档:Table Partitioning(Partition Maintenance 段)
- PostgreSQL 18 官方文档:ALTER TABLE(
DETACH PARTITION的CONCURRENTLY/FINALIZE) - PostgreSQL 18 官方文档:System Catalogs — pg_inherits(
inhdetachpending) - PostgreSQL 18 官方文档:Partition Information Functions(
pg_partition_tree/pg_partition_ancestors) - PostgreSQL 18 官方文档:TRUNCATE
- PostgreSQL 18 官方文档:pg_partman(PostgreSQL 生态工具)