创建与附加分区
CREATE TABLE ... PARTITION OF 与 ALTER TABLE ... ATTACH PARTITION 是把分区接进分区表的两种方式。前者一步到位「建表 + 挂载」,后者分两步「先建普通表(或加载数据),再挂载」。理解两者的差异,尤其是锁级别与数据校验扫描的差异,是在线运维大表分区的关键。
本页聚焦「如何安全地把分区接上去」:批量预建、ATTACH 的 CHECK 约束优化、DETACH 的并发形式、DEFAULT 分区的增删,以及如何用系统目录验证结果。
前置知识
阅读本节前,建议先了解:声明式分区概述、范围分区(RANGE)、多级分区(子分区)
基础概念
两种挂载方式的定位
| 方式 | 语法 | 适用场景 |
|---|---|---|
| 直接建分区 | CREATE TABLE p PARTITION OF parent FOR VALUES ... | 分区尚为空、随用随建的常规场景 |
| 先建后挂载 | CREATE TABLE t (...) + ALTER TABLE parent ATTACH PARTITION t FOR VALUES ... | 已有数据的大表接入、无锁预建、外部导入后挂载 |
两者的边界语法完全相同(都用 partition_bound_spec),区别在于:
CREATE TABLE ... PARTITION OF会对父表取ACCESS EXCLUSIVE锁,并在同一事务里完成建表与挂载;ATTACH PARTITION自 PostgreSQL 12 起对父表只需SHARE UPDATE EXCLUSIVE锁,且可以在建好表、灌好数据、建好索引之后再挂载;ATTACH PARTITION会扫描待附加表校验数据是否满足分区约束(除非有可用的CHECK约束),这是它的主要开销来源。
锁级别对照:为什么 ATTACH 更友好
官方文档对锁的说明非常明确:
the
ATTACH PARTITIONoperation requires only aSHARE UPDATE EXCLUSIVElock on the partitioned table rather than theACCESS EXCLUSIVElock required byCREATE TABLE ... PARTITION OF.
| 操作 | 父表锁 | 目标表锁 | 说明 |
|---|---|---|---|
CREATE TABLE ... PARTITION OF | ACCESS EXCLUSIVE | — | 阻塞父表上的所有读写 |
ATTACH PARTITION | SHARE UPDATE EXCLUSIVE(PG 12+) | ACCESS EXCLUSIVE | 不阻塞父表的 SELECT/INSERT,但仍需校验数据 |
DETACH PARTITION | ACCESS EXCLUSIVE | — | 阻塞父表读写 |
DETACH PARTITION CONCURRENTLY(PG 14+) | SHARE UPDATE EXCLUSIVE | — | 不阻塞父表读写,但限制多 |
SHARE UPDATE EXCLUSIVE 与 ACCESS SHARE(普通 SELECT)、ROW EXCLUSIVE(INSERT/UPDATE/DELETE)都不冲突,所以 ATTACH PARTITION 期间的读写可以正常进行。这就是在线给生产表补分区的可行路径。
ATTACH 的数据校验与 CHECK 优化
ATTACH PARTITION 必须确认待附加表里的所有行都满足分区约束,否则挂载后分区表的数据完整性就破了。默认做法是全表扫描。
如果待附加表上已经有一个蕴含分区约束的 CHECK 约束,PostgreSQL 就能直接依赖它、跳过扫描:
it is recommended that you create a
CHECKconstraint on the table to be attached that matches the expected partition constraint, ... this will allow the ATTACH PARTITION step to skip the scan. ... it is recommended to drop the now-redundant CHECK constraint after the ATTACH PARTITION is complete.
流程如下:
-- 1. 先建普通表(此时可以在线灌数据,无父表锁)
CREATE TABLE orders_2026_04 (
order_id bigint NOT NULL,
created_at timestamptz NOT NULL,
amount numeric(12,2)
);
-- 2. 加一个「匹配预期分区约束」的 CHECK —— 支持 NOT VALID 以避免扫描
ALTER TABLE orders_2026_04
ADD CONSTRAINT orders_2026_04_bound
CHECK (created_at >= '2026-04-01' AND created_at < '2026-05-01') NOT VALID;
-- 3. 灌数据(CHECK 存在,新增行会被强制校验)
-- 4. 校验 CHECK(扫描一次,把约束标为 valid)
ALTER TABLE orders_2026_04 VALIDATE CONSTRAINT orders_2026_04_bound;
-- 5. 挂载:因为 CHECK 已 valid 且蕴含分区约束,ATTACH 跳过扫描
ALTER TABLE orders ATTACH PARTITION orders_2026_04
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
-- 6. 挂载完成后删除冗余 CHECK
ALTER TABLE orders_2026_04 DROP CONSTRAINT orders_2026_04_bound;这个「先 NOT VALID 建约束 → 灌数据 → VALIDATE → ATTACH → 删约束」的序列,是把大表在最少的持锁时间内接入分区表的标准做法。
父表有 DEFAULT 分区时的额外扫描
如果父表已经有一个 DEFAULT 分区,ATTACH 的代价会更高:
If the partitioned table has a DEFAULT partition, it is recommended that you create a CHECK constraint on the default partition that ... excludes the rows that would fall into the new partition, ... otherwise the DEFAULT partition will be scanned.
因为挂载新分区后,原来落在 DEFAULT 里的行必须重新分配合适的分区,所以 DEFAULT 必须被扫描并可能与新分区发生行迁移——而且这个扫描是在父表的 ACCESS EXCLUSIVE 锁下进行的,影响更大。
-- 给 DEFAULT 分区加一个排除新分区范围的 CHECK,让 ATTACH 不必扫描它
ALTER TABLE orders_default
ADD CONSTRAINT orders_default_exclude_2026_04
CHECK (created_at < '2026-04-01' OR created_at >= '2026-05-01') NOT VALID;
ALTER TABLE orders_default VALIDATE CONSTRAINT orders_default_exclude_2026_04;
-- 之后 ATTACH 新分区时便可跳过 DEFAULT 分区扫描DETACH 的两种形式
DETACH PARTITION 把分区从父表摘下来:分区表本身不被删除,只是不再是这个父表的一部分。
| 形式 | 最低版本 | 父表锁 | 能否在事务块内 | 限制 |
|---|---|---|---|---|
DETACH PARTITION name | PG 10 | ACCESS EXCLUSIVE | 可以 | — |
DETACH PARTITION name CONCURRENTLY | PG 14 | SHARE UPDATE EXCLUSIVE | 不可以 | 父表不能有 DEFAULT 分区;同一分区不能并发 detach |
DETACH PARTITION name FINALIZE | PG 18 | — | 可以 | 用于完成被中断的 CONCURRENTLY 操作 |
-- 阻塞式(但简单、可在事务中与后续操作组合)
ALTER TABLE orders DETACH PARTITION orders_2025_01;
-- 并发式(PG 14+,不阻塞读写,但不可放入事务块)
ALTER TABLE orders DETACH PARTITION orders_2025_01 CONCURRENTLY;DETACH CONCURRENTLY 的关键限制:它不能在事务块(BEGIN ... COMMIT)中执行,且要求父表没有 DEFAULT 分区。若该操作被中断,会留下一个「正在 detach」的中间状态,需要用 PG 18 的 FINALIZE 收尾:
-- PG 18:完成一个被取消的 DETACH CONCURRENTLY
ALTER TABLE orders DETACH PARTITION orders_2025_01 FINALIZE;ATTACH 没有 CONCURRENTLY 形式
这是一个常见误解。ATTACH PARTITION 不存在 CONCURRENTLY 变体:
ALTER TABLE [ ONLY ] name [ * ] ATTACH PARTITION partition_name
{ FOR VALUES partition_bound_spec | DEFAULT }它对父表只需 SHARE UPDATE EXCLUSIVE(PG 12+)已经是「相对并发友好」的,其真正的开销是校验扫描,而这一点靠预建 CHECK 约束来消除,而不是靠 CONCURRENTLY。
系统目录与函数:如何验证结果
| 对象 | 用途 |
|---|---|
pg_inherits | 父子分区关系(inhparent / inhrelid) |
pg_class.relispartition | 该表是否为分区 |
pg_class.relpartbound | 分区边界(用 pg_get_expr() 展开) |
pg_partition_tree(regclass) | 递归列出整棵分区树(PG 12+) |
pg_partition_ancestors(regclass) | 列出某分区的祖先链(PG 12+) |
pg_partition_root(regclass) | 返回分区表的根(PG 12+) |
-- 确认新分区已挂载,并读出边界
SELECT c.relname, pg_get_expr(c.relpartbound, c.oid) AS bound
FROM pg_class c
JOIN pg_inherits i ON i.inhrelid = c.oid
JOIN pg_class p ON p.oid = i.inhparent
WHERE p.relname = 'orders'
ORDER BY c.relname;语法与用法
直接建分区
CREATE TABLE [ IF NOT EXISTS ] table_name
PARTITION OF parent_table [ ( column_name [, ...] ) ]
{ FOR VALUES partition_bound_spec | DEFAULT }
[ PARTITION BY partition_method ]
[ TABLESPACE tablespace_name ]其中 partition_bound_spec 为:
IN ( partition_bound_expr [, ...] ) |
FROM ( { partition_bound_expr | MINVALUE | MAXVALUE } [, ...] )
TO ( { partition_bound_expr | MINVALUE | MAXVALUE } [, ...] ) |
WITH ( MODULUS numeric_literal, REMAINDER numeric_literal )附加已有表
ALTER TABLE [ ONLY ] parent_table [ * ]
ATTACH PARTITION partition_name
{ FOR VALUES partition_bound_spec | DEFAULT };
ALTER TABLE parent_table
DETACH PARTITION partition_name
[ CONCURRENTLY | FINALIZE ];建父表并直接建分区
CREATE TABLE orders (
order_id bigint NOT NULL,
region text NOT NULL,
created_at timestamptz NOT NULL,
amount numeric(12,2) NOT NULL,
PRIMARY KEY (created_at, order_id)
) 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 orders_default PARTITION OF orders DEFAULT;postgres=# \d+ orders
Partitioned table "public.orders"
Column | Type | Collation | Nullable | Default
------------+--------------------------+-----------+----------+---------
order_id | bigint | | not null |
region | text | | not null |
created_at | timestamp with time zone | | not null |
amount | numeric(12,2) | | not null |
Partition key: RANGE (created_at)
Indexes:
"orders_pkey" PRIMARY KEY, btree (created_at, order_id)
Number of partitions: 3 (Use \d+ to list them.)先建后 ATTACH
-- 建一个独立表并灌数据(此时与父表无关,不持父表锁)
CREATE TABLE orders_2026_03 (
order_id bigint NOT NULL,
region text NOT NULL,
created_at timestamptz NOT NULL,
amount numeric(12,2) NOT NULL
);
-- 建议建匹配分区约束的 CHECK,以便 ATTACH 跳过扫描
ALTER TABLE orders_2026_03
ADD CONSTRAINT orders_2026_03_bound
CHECK (created_at >= '2026-03-01' AND created_at < '2026-04-01') NOT VALID;
ALTER TABLE orders_2026_03 VALIDATE CONSTRAINT orders_2026_03_bound;
-- 附加
ALTER TABLE orders ATTACH PARTITION orders_2026_03
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- 删除冗余约束
ALTER TABLE orders_2026_03 DROP CONSTRAINT orders_2026_03_bound;ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE验证挂载结果
SELECT c.relname,
c.relispartition,
pg_get_expr(c.relpartbound, c.oid) AS bound
FROM pg_class c
WHERE c.relname IN ('orders_2026_01','orders_2026_02','orders_2026_03','orders_default')
ORDER BY c.relname; relname | relispartition | bound
------------------+----------------+-------------------------------------------------
orders_2026_01 | t | FOR VALUES FROM ('2026-01-01 08:00:00+08') TO (...)
orders_2026_02 | t | FOR VALUES FROM ('2026-02-01 08:00:00+08') TO (...)
orders_2026_03 | t | FOR VALUES FROM ('2026-03-01 08:00:00+08') TO (...)
orders_default | t | DEFAULT用 pg_partition_tree 查看树
SELECT relid::regclass AS name, relkind, level
FROM pg_partition_tree('orders')
ORDER BY level, name; name | relkind | level
-------------------+---------+-------
orders | p | 0
orders_2026_01 | r | 1
orders_2026_02 | r | 1
orders_2026_03 | r | 1
orders_default | r | 1批量预建分区
用 format() + DO 块可以一次预建多个时间段的分区:
DO $$
DECLARE
start_month date := date_trunc('month', now())::date;
m date;
part_name text;
BEGIN
FOR i IN 0..11 LOOP
m := (start_month + (i || ' month')::interval)::date;
part_name := 'orders_' || to_char(m, 'YYYY_MM');
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF orders FOR VALUES FROM (%L) TO (%L)',
part_name, m, (m + interval '1 month')::date);
END LOOP;
END $$;DO%I 用于标识符(表名),%L 用于字面量(日期),二者不能混用——用错会导致 SQL 注入或语法错误。
DETACH / 重新 ATTACH
-- 摘下(阻塞式,可在事务内)
BEGIN;
ALTER TABLE orders DETACH PARTITION orders_2025_12;
DROP TABLE orders_2025_12;
COMMIT;
-- 也可以摘下后保留为独立表
ALTER TABLE orders DETACH PARTITION orders_2026_01;DETACH TABLE
DROP TABLE详细说明
CREATE ... PARTITION OF 的锁代价
CREATE TABLE ... PARTITION OF 会在父表上取 ACCESS EXCLUSIVE 锁,阻塞父表上的所有读写。这意味着:在高频写入的父表上「随时新建下个月分区」,会造成一次短暂的写阻塞。
-- 挂载时若父表上有长事务,这个 CREATE 会一直等待
CREATE TABLE orders_2026_05 PARTITION OF orders
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');缓解方式:
- 提前预建分区(在低峰期做),而不是等到插入报错才补;
- 若分区已建好数据,优先用
ATTACH PARTITION(父表锁更轻)。
ATTACH 的 CHECK 优化:一步步验证
CHECK 优化的核心是让规划器「相信」待附加表的行都在边界内,从而跳过全表扫描。验证方式是对比有无 CHECK 时的耗时:
-- 无 CHECK:ATTACH 会扫描整表
ALTER TABLE orders ATTACH PARTITION orders_2026_06
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- 若 orders_2026_06 有 5000 万行,这一步会很慢
-- 有 CHECK(valid)时,规划器直接信任约束,跳过扫描可以用 EXPLAIN 观察校验查询本身:
-- 校验查询会形如:
EXPLAIN (COSTS OFF)
SELECT 1 FROM orders_2026_03 WHERE NOT (created_at >= '2026-03-01' AND created_at < '2026-04-01'); QUERY PLAN
---------------------------------------------------------
Result
One-Time Filter: false ← 有可用 CHECK 时,约束直接推出恒假父表 DEFAULT 分区存在时的 ATTACH 行为
当父表有 DEFAULT 分区,ATTACH 新分区会触发对 DEFAULT 分区的检查与可能的行迁移。这个检查在父表 ACCESS EXCLUSIVE 锁下进行:
-- 若 orders_default 里有大量数据,这一步会很慢且持锁
ALTER TABLE orders ATTACH PARTITION orders_2026_03
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');官方建议给 DEFAULT 分区加一个排除新分区范围的 CHECK 约束来跳过扫描:
ALTER TABLE orders_default
ADD CONSTRAINT orders_default_no_2026_03
CHECK (created_at < '2026-03-01' OR created_at >= '2026-04-01') NOT VALID;
ALTER TABLE orders_default VALIDATE CONSTRAINT orders_default_no_2026_03;
-- 现在 ATTACH 可跳过 DEFAULT 扫描
ALTER TABLE orders ATTACH PARTITION orders_2026_03
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');有 DEFAULT 分区时,新增分区可能因「数据冲突」失败
如果 DEFAULT 分区里已经有「本应属于新分区」的行,ATTACH 会报错:
ERROR: updated partition constraint for default partition "orders_default" would be violated by some row处理办法是:先 DETACH 掉 DEFAULT、把冲突行搬到新分区、再挂回 DEFAULT。这个流程在列表分区(LIST)中也有演示。
无锁预建分区:在线接入大表
把「新建 → 灌数据 → 建索引 → 挂载」拆开,可以把大部分耗时的工作放在不持父表锁的阶段完成:
# 阶段 A:建独立表并灌数据(不涉及父表,零阻塞)
psql -c "CREATE TABLE orders_2026_07 (LIKE orders INCLUDING ALL)"
# 阶段 B:灌入历史数据(可分批、可并发)
psql -c "COPY orders_2026_07 FROM '/data/orders_2026_07.csv' CSV"
# 阶段 C:建索引(普通表建索引同样是独立的)
psql -c "CREATE INDEX ON orders_2026_07 (region)"-- 阶段 D:加 CHECK 并 VALIDATE(扫描发生在独立表上,不影响父表)
ALTER TABLE orders_2026_07 ADD CONSTRAINT orders_2026_07_bound
CHECK (created_at >= '2026-07-01' AND created_at < '2026-08-01') NOT VALID;
ALTER TABLE orders_2026_07 VALIDATE CONSTRAINT orders_2026_07_bound;
-- 阶段 E:ATTACH(父表只需 SHARE UPDATE EXCLUSIVE,且因 CHECK 已 valid 而跳过扫描)
ALTER TABLE orders ATTACH PARTITION orders_2026_07
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
-- 阶段 F:清理冗余 CHECK
ALTER TABLE orders_2026_07 DROP CONSTRAINT orders_2026_07_bound;注意 LIKE orders INCLUDING ALL 会复制父表的列定义、默认值、约束、索引(但不会复制分区结构),是建待附加表的便捷起点。
ATTACH 的前提条件
ATTACH PARTITION 会做多项检查,任何一项不满足都会失败:
| 检查项 | 必须满足 |
|---|---|
| 列结构 | 待附加表与父表的列名、类型、排序规则兼容 |
| 列数 | 一致(顺序可不同,但 INCLUDING 与父表列集合必须匹配) |
NOT NULL | 分区键列在待附加表上应当 NOT NULL(否则需有 CHECK 保证非空) |
| 边界 | partition_bound_spec 不与现有分区重叠 |
| 表达式 | partition_bound_expr 类型与分区键列类型一致 |
| 约束 | 待附加表的约束与父表兼容 |
-- 不兼容时的典型错误
ALTER TABLE orders ATTACH PARTITION orders_bad
FOR VALUES FROM ('2026-01-15') TO ('2026-02-15');ERROR: partition "orders_bad" would overlap with existing partition "orders_2026_01"用 LIKE ... INCLUDING ALL 简化建表
从父表 LIKE 出待附加表可以保证列结构一致:
-- 从父表复制完整列定义(含默认值与约束),得到一张普通表
CREATE TABLE orders_2026_08 (LIKE orders INCLUDING ALL);
-- 注意:LIKE 不会复制分区属性
SELECT relkind FROM pg_class WHERE relname = 'orders_2026_08'; relkind
---------
r ← 普通表,可独立灌数据与建索引不过 LIKE ... INCLUDING ALL 在分区表上不会带上分区键约束,仍需显式加 CHECK 才能享受 ATTACH 的跳扫优化。
DEFAULT 分区的增删
DEFAULT 分区可以随时增删,但删除前要确认它里面没有不可丢弃的数据:
-- 新增 DEFAULT 分区
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
-- 删除(注意:DROP TABLE 会连数据一起删!)
DROP TABLE orders_default;
-- 若只想让它不再是分区但保留数据,用 DETACH 而非 DROP
ALTER TABLE orders DETACH PARTITION orders_default;不能直接对含数据的 DEFAULT 分区做 DETACH + ATTACH 切换
若 DEFAULT 分区内有数据,且你想为其中某一部分建立专属分区,必须按「DETACH DEFAULT → 建新分区 → 搬数据 → 挂回 DEFAULT」的完整流程来,且期间父表会持锁。
pg_partman:自动化预建与轮转
手工预建分区在分区数量多时难以维护。pg_partman 扩展提供了自动创建、轮转、清理的能力:
-- 为 orders 配置按月自动维护(示意,具体 API 随版本变化)
SELECT partman.create_parent(
p_parent_table => 'public.orders',
p_control => 'created_at',
p_type => 'range',
p_interval => '1 month',
p_premake => 3 -- 提前预建 3 个月
);p_premake 决定提前创建多少个未来分区,是防止「插入时才发现没有分区」的关键参数。
系统目录巡检脚本
定期检查分区是否齐备、是否有分区缺失或异常:
-- 列出分区表的所有直接子分区及其边界
SELECT c.relname AS partition,
pg_get_expr(c.relpartbound, c.oid) AS bound,
pg_size_pretty(pg_total_relation_size(c.oid)) AS size
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
JOIN pg_class p ON p.oid = i.inhparent
WHERE p.relname = 'orders'
ORDER BY c.relname; partition | bound | size
----------------+----------------------------------+---------
orders_2026_01 | FOR VALUES FROM ('2026-01-01'... | 1024 MB
orders_2026_02 | FOR VALUES FROM ('2026-02-01'... | 987 MB
orders_2026_03 | FOR VALUES FROM ('2026-03-01'... | 45 MB
orders_default | DEFAULT | 8192 kB检查是否存在「边界空洞」(例如 3 月和 4 月之间没有分区):
-- 用 pg_partition_tree 列出全部叶子,人工核对连续性
SELECT relid::regclass, pg_get_expr(relpartbound, relid) AS bound
FROM pg_partition_tree('orders')
WHERE relkind = 'r'
ORDER BY relid::regclass;实战示例
场景:一张已运行的生产表 orders,需要把它改造成按月分区,且不能停机。当前已有 2026 年 1~6 月的数据在单表里。
第 1 步:评估现状与目标结构
SELECT pg_size_pretty(pg_total_relation_size('orders')) AS size,
(SELECT count(*) FROM orders) AS rows; size | rows
---------+----------
42 GB | 89000000目标:改造为 RANGE(created_at) 月度分区,6 个历史月 + 未来预建 3 个月。
第 2 步:创建分区父表(新名字,避免锁旧的单表)
CREATE TABLE orders_new (
order_id bigint NOT NULL,
region text NOT NULL,
created_at timestamptz NOT NULL,
amount numeric(12,2) NOT NULL,
PRIMARY KEY (created_at, order_id)
) PARTITION BY RANGE (created_at);第 3 步:逐月把历史数据搬入待附加表
DO $$
DECLARE
m date;
part text;
BEGIN
FOR i IN 0..5 LOOP
m := (DATE '2026-01-01' + (i || ' month')::interval)::date;
part := 'orders_new_' || to_char(m, 'YYYY_MM');
-- 建独立表
EXECUTE format('CREATE TABLE %I (LIKE orders_new INCLUDING ALL)', part);
-- 加 CHECK(含边界),以便后续 ATTACH 跳过扫描
EXECUTE format(
'ALTER TABLE %I ADD CONSTRAINT %I CHECK (created_at >= %L AND created_at < %L) NOT VALID',
part, part || '_bound', m, (m + interval '1 month')::date);
-- 从旧表搬数据
EXECUTE format(
'INSERT INTO %I SELECT * FROM orders WHERE created_at >= %L AND created_at < %L',
part, m, (m + interval '1 month')::date);
-- 校验 CHECK
EXECUTE format('ALTER TABLE %I VALIDATE CONSTRAINT %I', part, part || '_bound');
END LOOP;
END $$;第 4 步:附加 6 个历史分区(父表仅 SHARE UPDATE EXCLUSIVE)
DO $$
DECLARE
m date;
part text;
BEGIN
FOR i IN 0..5 LOOP
m := (DATE '2026-01-01' + (i || ' month')::interval)::date;
part := 'orders_new_' || to_char(m, 'YYYY_MM');
EXECUTE format('ALTER TABLE orders_new ATTACH PARTITION %I FOR VALUES FROM (%L) TO (%L)',
part, m, (m + interval '1 month')::date);
END LOOP;
END $$;第 5 步:预建未来 3 个月分区
DO $$
DECLARE
m date;
part text;
BEGIN
FOR i IN 0..2 LOOP
m := (DATE '2026-07-01' + (i || ' month')::interval)::date;
part := 'orders_new_' || to_char(m, 'YYYY_MM');
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF orders_new FOR VALUES FROM (%L) TO (%L)',
part, m, (m + interval '1 month')::date);
END LOOP;
END $$;第 6 步:验证结构与裁剪
SELECT relid::regclass AS name, relkind, level
FROM pg_partition_tree('orders_new')
ORDER BY level, name; name | relkind | level
--------------------+---------+-------
orders_new | p | 0
orders_new_2026_01 | r | 1
...
orders_new_2026_09 | r | 1EXPLAIN (COSTS OFF)
SELECT * FROM orders_new WHERE created_at >= '2026-03-01' AND created_at < '2026-04-01'; QUERY PLAN
--------------------------------------------------
Seq Scan on orders_new_2026_03
Filter: (created_at >= ... AND created_at < ...)第 7 步:切换(RENAME)
数据全部搬完并验证后,用一次短暂的 ACCESS EXCLUSIVE 完成原子切换:
BEGIN;
ALTER TABLE orders RENAME TO orders_old;
ALTER TABLE orders_new RENAME TO orders;
COMMIT;第 8 步:清理旧表
-- 确认业务正常后,删除旧表回收空间
DROP TABLE orders_old;注意事项
ATTACH PARTITION没有CONCURRENTLY形式。 它的并发友好性来自父表的SHARE UPDATE EXCLUSIVE锁(PostgreSQL 12+),校验开销靠预建CHECK约束消除。ATTACH会扫描待附加表校验数据。 大表上不加CHECK直接挂载可能耗时数分钟并长时间持锁。sql-- 错误!思路是:以为 ATTACH 是廉价操作 ALTER TABLE orders ATTACH PARTITION orders_huge FOR VALUES FROM ('2026-01-01') TO ('2027-01-01'); -- 正确做法:先加 CHECK 并 VALIDATE,再 ATTACHDEFAULT分区存在时,ATTACH还要扫描DEFAULT。 建议给DEFAULT加排除新分区范围的CHECK。sqlALTER TABLE orders_default ADD CONSTRAINT orders_default_excl_2026_03 CHECK (created_at < '2026-03-01' OR created_at >= '2026-04-01') NOT VALID; ALTER TABLE orders_default VALIDATE CONSTRAINT orders_default_excl_2026_03;DETACH PARTITION CONCURRENTLY不能在事务块中执行。sql-- 错误!不能在 BEGIN/COMMIT 中使用 CONCURRENTLY BEGIN; ALTER TABLE orders DETACH PARTITION orders_2025_12 CONCURRENTLY; COMMIT; -- 正确:单独执行 ALTER TABLE orders DETACH PARTITION orders_2025_12 CONCURRENTLY;DETACH PARTITION CONCURRENTLY要求父表没有DEFAULT分区。textERROR: cannot detach partition "orders_default" concurrently DETAIL: Cannot detach the default partition concurrently.DROP TABLE分区会连数据一起删除;只想摘链请用DETACH。sql-- 危险!这是彻底删除数据和表 DROP TABLE orders_default; -- 安全:只摘链、保留数据 ALTER TABLE orders DETACH PARTITION orders_default;ATTACH的边界必须与现有分区不重叠、且与父表分区键类型匹配。textERROR: partition "orders_bad" would overlap with existing partition "orders_2026_01"format()中标识符与字面量占位符不可混用。%I给标识符、%L给字面量,混用会导致语法错误甚至注入风险。sql-- 错误!表名用了 %L 会带上引号,成为字符串而非标识符 EXECUTE format('CREATE TABLE %L PARTITION OF orders ...', part_name);不要等到插入报错才建分区。 应用侧遇到
no partition of relation ... found for row就是一次失败写入。textERROR: no partition of relation "orders" found for row DETAIL: Partition key of the failing row contains (created_at) = (2026-10-05 00:00:00+08).ATTACH期间父表虽不阻塞读写,但仍会取SHARE UPDATE EXCLUSIVE。 该锁与VACUUM FULL、CREATE INDEX CONCURRENTLY的清理阶段冲突,需避开这些维护窗口。
最佳实践
提前预建分区,而不是被动补建。 用
pg_partman的p_premake或自建脚本,保证始终有未来若干月的分区存在。sql-- 用 IF NOT EXISTS 让预建脚本可重复执行 CREATE TABLE IF NOT EXISTS orders_2026_10 PARTITION OF orders FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');大表接入一律走「先建后 ATTACH」路线。 把灌数据与建索引放在不持父表锁的阶段完成。
待附加表上先建匹配边界的
CHECK约束,ATTACH后删除。 这是跳过校验扫描的官方推荐做法。sqlALTER TABLE orders_2026_04 ADD CONSTRAINT orders_2026_04_bound CHECK (created_at >= '2026-04-01' AND created_at < '2026-05-01') NOT VALID; ALTER TABLE orders_2026_04 VALIDATE CONSTRAINT orders_2026_04_bound; -- ATTACH 之后 ALTER TABLE orders_2026_04 DROP CONSTRAINT orders_2026_04_bound;父表有
DEFAULT分区时,为它建排除约束。 避免ATTACH时扫描DEFAULT导致长时间持锁。日常维护优先使用
DETACH PARTITION CONCURRENTLY。 归档下线旧分区时避免阻塞业务读写(注意不能在事务块内、且父表无DEFAULT分区)。sqlALTER TABLE orders DETACH PARTITION orders_2025_12 CONCURRENTLY;用系统目录函数做挂载后验证。 每次
ATTACH/DETACH后立刻核对边界与层级。sqlSELECT relid::regclass, pg_get_expr(relpartbound, relid) AS bound FROM pg_partition_tree('orders') WHERE relkind = 'r';批量 DDL 用
DO+format(),并区分%I/%L。 可重复执行(IF NOT EXISTS)、可审计。预建脚本纳入版本管理与定时任务。 分区创建本质上是「提前完成的运维动作」,应该由调度器执行而非人工记忆。
改造大表时用「新表 + 双写/回填 + 重命名」的经典迁移法。 借
RENAME在极短窗口内完成切换,出问题可快速回退。为分区表建立监控:每月分区是否齐备、
DEFAULT分区是否积累了非预期数据。DEFAULT分区体积异常增长,往往意味着某个分区被漏建。sql-- 监控 DEFAULT 分区大小异常 SELECT pg_size_pretty(pg_total_relation_size('orders_default')) AS default_size;
下一节
下一节:分区裁剪(Partition Pruning)——理解分区裁剪的原理、静态与动态裁剪的差别,以及如何写出「能被裁剪」的查询。
参考链接
- PostgreSQL 18 官方文档:Table Partitioning(含 ATTACH 锁与 CHECK 优化说明)
- PostgreSQL 18 官方文档:CREATE TABLE(
PARTITION OF与partition_bound_spec) - PostgreSQL 18 官方文档:ALTER TABLE(
ATTACH/DETACH PARTITION与CONCURRENTLY/FINALIZE) - PostgreSQL 18 官方文档:Partition Information Functions(
pg_partition_tree等) - PostgreSQL 18 官方文档:System Catalogs — pg_inherits
- PostgreSQL 18 官方文档:String Functions — format()