Skip to content

分区维护(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 回收死元组。删一百亿行,就是一百亿次行级操作。

分区表把「一批行」变成了「一个文件集合」。删除整月数据只要把承载它的分区摘掉:

text
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+)内部用两个事务实现「不阻塞」:

text
事务 1(SHARE UPDATE EXCLUSIVE 锁)
  ├─ 在 pg_inherits.inhdetachpending 上把该分区标为「正在摘除」
  └─ 提交后,新查询的规划期不再把它当作分区;已执行中的查询仍可安全访问

事务 2(独立事务)
  ├─ 真正解除继承关系,刷新缓存
  └─ 完成后分区成为普通表

正因为它跨两个事务,不能在事务块内执行,也不能放进存储过程。如果第二个事务因崩溃或取消而没跑完,分区会停在「pending detach」状态,需要用 FINALIZE 补齐。

pending detach 与 FINALIZE ​

pg_inherits.inhdetachpending 列(bool)标记该分区正在被摘除。官方对 FINALIZE 的定义是:完成一次此前被取消或中断的 DETACH CONCURRENTLY,并且同一时刻一张分区表最多只能有一个分区处于 pending detach 状态。

sql
-- 查看是否有分区停在 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 partitionACCESS EXCLUSIVE阻塞父表所有读写
DETACH PARTITIONACCESS EXCLUSIVE阻塞父表所有读写
DETACH PARTITION CONCURRENTLYSHARE UPDATE EXCLUSIVE不阻塞读写,但限制多
DETACH PARTITION ... FINALIZESHARE UPDATE EXCLUSIVE完成中断的并发摘除
TRUNCATE partition分区自身 ACCESS EXCLUSIVE不经父表,但会打断使用该分区的查询

注意

DROP TABLE 与 DETACH 都取父表 ACCESS EXCLUSIVE 锁,必须等所有引用该父表的查询结束才能拿到。长事务、长查询会让这一步排队,进而阻塞它后面排队的全部查询。维护窗口要避开长事务。

语法与用法 ​

text
-- 直接删除分区(连带数据)
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;

最简可运行示例:

sql
-- 先用一个 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');
text
CREATE TABLE
CREATE TABLE
CREATE TABLE
sql
-- 灌入一点数据,便于观察行数与大小
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');
text
INSERT 0 1
INSERT 0 1
INSERT 0 1

方式一:直接 DROP(数据不需要了)

sql
DROP TABLE orders_2026_01;
text
DROP TABLE

方式二:先 DETACH 再处理(数据还要归档)

sql
ALTER TABLE orders DETACH PARTITION orders_2026_02;
-- 此时 orders_2026_02 仍是普通表,数据完好
SELECT count(*) FROM orders_2026_02;
text
ALTER TABLE
 count
-------
     1
(1 row)

方式三:并发摘除(PostgreSQL 14+,在线归档);方式四:清空分区但不删表

sql
ALTER TABLE orders DETACH PARTITION orders_2026_02 CONCURRENTLY;  -- 不阻塞父表读写
TRUNCATE orders_2026_02;                                          -- 只清行、保留表
text
ALTER TABLE
TRUNCATE TABLE

维护后确认结构 ​

sql
SELECT relid::regclass AS partition, level, isleaf
FROM pg_partition_tree('orders');

SELECT relid::regclass AS ancestor
FROM pg_partition_ancestors('orders_2026_02');
text
   partition   | level | isleaf
---------------+-------+--------
 orders        |     0 | f
 orders_2026_02|     1 | t
(2 rows)

   ancestor
-------------
 orders_2026_02
 orders
(2 rows)

详细说明 ​

DROP TABLE:最快的数据清理 ​

对分区执行 DROP TABLE 只涉及元数据改写与文件删除,不逐行处理。官方原文强调它「可以非常快地删除数百万条记录,因为它不必逐行删除」。

sql
-- 删除 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 备份,或把明细聚合成更小的汇总格式。

sql
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 时可以直接跳过校验扫描。

sql
SELECT conname, pg_get_constraintdef(oid) AS def
FROM pg_constraint
WHERE conrelid = 'orders_2026_01'::regclass AND contype = 'c';
text
     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多个并发摘除需要串行
sql
-- 正确:作为顶层单条命令执行
ALTER TABLE orders DETACH PARTITION orders_2025_12 CONCURRENTLY;
text
ALTER TABLE

在事务块内执行会被明确拒绝:

text
ERROR:  ALTER TABLE ... DETACH CONCURRENTLY cannot run inside a transaction block

父表存在 DEFAULT 分区时同样被拒绝:

text
ERROR:  cannot detach partitions concurrently when a default partition exists

因此「想用并发摘除」与「保留 DEFAULT 分区兜底」在设计上互斥,需要提前取舍。

DETACH ... FINALIZE 与 pending detach 的恢复 ​

如果 CONCURRENTLY 的第二个事务被取消、连接中断或实例崩溃,分区会停在 pending detach 状态:它对新查询已不可见,但继承关系还没真正解除。

sql
-- 巡检:找出停在 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;
text
   partition   |  size
---------------+--------
 orders_2025_12| 8192 kB
(1 row)

恢复动作就是补一条 FINALIZE:

sql
ALTER TABLE orders DETACH PARTITION orders_2025_12 FINALIZE;
text
ALTER TABLE

FINALIZE 也只需父表 SHARE UPDATE EXCLUSIVE 锁,可以放心在业务时段补执行。若巡检结果为空,说明没有未完成的摘除。

TRUNCATE:清空单个分区 ​

TRUNCATE 丢掉分区内的全部行但保留表结构,比 DELETE 快,也不需要 VACUUM。它取分区自身的 ACCESS EXCLUSIVE 锁,不经父表,因此不会阻塞父表上的其他分区访问,但会打断正在使用该分区的查询。它还是事务性的,可以回滚。

sql
-- 只清空一个分区;在事务内可回滚
BEGIN;
TRUNCATE orders_2026_02;
ROLLBACK;
SELECT count(*) FROM orders_2026_02;   -- 行数不变
text
BEGIN
TRUNCATE TABLE
ROLLBACK
 count
-------
     1
(1 row)

TRUNCATE 加到父表上会级联清空所有分区,这是常见的误操作来源:

sql
-- 危险!这会清空 orders 的全部数据
TRUNCATE orders;
text
TRUNCATE TABLE

对比 DELETE / TRUNCATE / DETACH / DROP ​

同一批数据,四条路径的成本天差地别。以「删除一整月、约 2 亿行」为例:

维度DELETETRUNCATEDETACHDROP
行级写 WAL是否否否
索引维护逐项整文件重建随表脱离随表消失
事后 VACUUM必须不需要不需要不需要
数据可保留否否是否
父表锁行级无(锁分区)ACCESS EXCLUSIVE / SHARE UPDATE EXCLUSIVEACCESS EXCLUSIVE
典型耗时小时级秒级毫秒级毫秒级
sql
-- 不推荐:整月清理却用 DELETE,留下巨量死元组
DELETE FROM orders WHERE created_at >= '2026-02-01' AND created_at < '2026-03-01';
text
DELETE 200000000
Time: 1834521.203 ms
sql
-- 推荐:整月清理直接摘分区
ALTER TABLE orders DETACH PARTITION orders_2026_02;
text
ALTER TABLE
Time: 8.412 ms

需要强调:只有当条件恰好等于某个分区的边界时才能整分区摘除。若条件跨越多个分区,可先拆成多个整分区操作,只对残余的边界分区用 DELETE。

索引、约束与触发器的随动 ​

DETACH 与 DROP 会自动处理从父表传播下来的对象,无需手工清理:

sql
-- 父表上的索引会传播到每个分区
CREATE INDEX orders_created_idx ON orders (created_at);
text
CREATE INDEX
sql
-- 摘除后,分区的索引跟着脱离,但依然存在(它仍是独立表的索引)
ALTER TABLE orders DETACH PARTITION orders_2026_02;

SELECT indexrelid::regclass AS idx
FROM pg_index
WHERE indrelid = 'orders_2026_02'::regclass;
text
                idx
-----------------------------------
 orders_2026_02_created_at_idx
(1 row)

若分区本身还有二级子分区,DETACH 会把整棵子树一起摘下——脱离后的根成为一棵独立的分区树,而不是被拆散。

sql
-- 查看子树规模,确认摘除影响范围
SELECT count(*) AS leaf_count
FROM pg_partition_tree('orders_2026_02')
WHERE isleaf;
text
 leaf_count
------------
          1
(1 row)

用 pg_partman 自动化轮转 ​

手工建分区、手工摘除在几十个分区时还能应付,上百个就必须交给工具。pg_partman 是社区事实标准,它在 part_config 表中登记每个父表的轮转策略。

sql
-- 在 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';
text
   create_parent
------------------
 public.orders
(1 row)

UPDATE 1

之后由定时任务调用 run_maintenance(),它一次完成「预建未来分区 + 清理过期分区」:

sql
CALL partman.run_maintenance(p_analyze => false);
text
CALL
sql
-- 查看登记与运行状态
SELECT parent_table, partition_interval, premake, retention, retention_keep_table
FROM partman.part_config;
text
 parent_table  | partition_interval | premake | retention  | retention_keep_table
---------------+--------------------+---------+------------+----------------------
 public.orders | 1 mon              |       4 | 12 months  | f
(1 row)

分区数量治理 ​

分区不是越多越好。每个分区在规划期都要参与裁剪判断、每次 DDL 都要遍历整棵分区树,分区数上千后规划开销与锁范围都会显著恶化。

sql
-- 统计每个父表的分区数与总大小
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;
text
 parent_table | partition_count | total_size
--------------+-----------------+------------
 orders       |             240 | 412 GB
(1 row)

按月分区攒满 20 年就是 240 个分区,此时应考虑合并粒度(月 → 季)或引入多级分区,把一级分区数控制在几十的量级。同时要能识别「空分区」——对分区执行 SELECT count(*) 复核真实行数,确认后再合并或删除,避免误删刚预建、尚未写入的未来分区。

维护脚本:批量生成与巡检 ​

日常维护应固化成两类脚本:批量生成 DDL 与结构巡检。前者用 format() 的 %I(标识符)与 %L(字面量)拼接,后者用系统目录函数核对结果。

sql
-- 批量预建未来 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 $$;
text
DO
sql
-- 巡检:各分区的边界、行数与大小
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;
text
   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 分区是否异常增长。它变大通常意味着某个分区被漏建。

sql
SELECT pg_size_pretty(pg_total_relation_size('orders_default')) AS default_size,
       (SELECT count(*) FROM orders_default) AS default_rows;
text
 default_size | default_rows
--------------+--------------
 142 MB       |       812345
(1 row)

把上面的动作串起来,一个典型的时间序列分区表每日/每月维护节奏是:

text
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 分区体积、分区总数
bash
# 用 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 步:建立带索引的月度分区表 ​

sql
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);
text
CREATE TABLE
CREATE INDEX

第 2 步:预建 2026 年 1—3 月分区 ​

sql
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');
text
CREATE TABLE
CREATE TABLE
CREATE TABLE

第 3 步:写入数据并确认路由 ​

sql
INSERT INTO orders
SELECT g, 1000 + g % 100, (g % 500)::numeric, DATE '2026-01-01' + (g % 90)
FROM generate_series(1, 100000) AS g;
text
INSERT 0 100000
sql
SELECT tableoid::regclass AS partition, count(*)
FROM orders GROUP BY 1 ORDER BY 1;
text
   partition    | count
----------------+-------
 orders_2026_01 | 33333
 orders_2026_02 | 30333
 orders_2026_03 | 36334
(3 rows)

第 4 步:在线摘除最旧分区(并发) ​

sql
ALTER TABLE orders DETACH PARTITION orders_2026_01 CONCURRENTLY;
text
ALTER TABLE

此语句只需父表 SHARE UPDATE EXCLUSIVE 锁,期间父表读写不受影响。

第 5 步:确认 pending detach 状态并巡检 ​

sql
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;
text
   partition    | inhdetachpending
----------------+------------------
 orders_2026_02 | f
 orders_2026_03 | f
(2 rows)

orders_2026_01 已不在列表中,说明摘除彻底完成。

第 6 步:归档并删除 ​

bash
# 摘除后的表是普通表,可直接逻辑导出
pg_dump -d appdb -t orders_2026_01 -Fc -f /data/archive/orders_2026_01.dump
sql
DROP TABLE orders_2026_01;
text
DROP TABLE

第 7 步:清理后确认剩余分区仍可用 ​

sql
SELECT count(*) FROM orders WHERE created_at >= '2026-02-01' AND created_at < '2026-03-01';
text
 count
-------
 30333
(1 row)
sql
EXPLAIN (COSTS OFF)
SELECT count(*) FROM orders WHERE created_at >= '2026-03-01' AND created_at < '2026-04-01';
text
                  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。 这是内核层面的限制,不是配置问题。

    text
    ERROR:  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,在线完成下线。

    sql
    ALTER TABLE orders DETACH PARTITION orders_2026_01 CONCURRENTLY;
  • 维护脚本必须先巡检 pending detach,再执行新摘除。 保证同一父表不会有第二个摘除排队。

    sql
    SELECT 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。

    bash
    pg_dump -d appdb -t orders_2026_01 -Fc -f /data/archive/orders_2026_01.dump
  • 预建分区交给 pg_partman 或调度脚本,不要靠人工记忆。 p_premake 保证始终有未来若干期分区存在。

    sql
    SELECT 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 则只摘链保留。

    sql
    UPDATE partman.part_config
    SET retention = '12 months', retention_keep_table = false
    WHERE parent_table = 'public.orders';
  • 把结构巡检固化为一条 SQL。 分区总数、DEFAULT 分区体积、pending detach 三项合并巡检,异常即为漏建或未收尾。

    sql
    SELECT (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。 保证脚本可重复执行、可审计、无注入风险。

    sql
    EXECUTE 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+)。

    sql
    SELECT relid::regclass, level, isleaf FROM pg_partition_tree('orders') ORDER BY level, relid;

下一节

下一节:分区索引策略(Partitioned Indexes)——理解分区表上的索引如何传播、唯一约束为何必须包含分区键,以及如何无阻塞地为大分区表建索引。

参考链接 ​