列表分区(LIST)
列表分区(List Partitioning)把分区键的取值逐个枚举到指定的分区里:FOR VALUES IN ('a','b') 表示这个分区接收所有分区键等于 a 或 b 的行。它没有区间概念,也不做取模计算,是三种声明式分区中语义最直白的一种——「这个分区装哪些值」一眼就能从 DDL 里读出来。
列表分区的典型场景是离散维度:地区(region)、国家代码、租户(tenant)、业务线、状态枚举。这些列的取值是有限、枚举、可数的,天然适合按值归档,而不是按时间轮转。
前置知识
阅读本节前,建议先了解:声明式分区概述、范围分区(RANGE)
基础概念
列表分区在解决什么问题
范围分区擅长处理「连续」的维度(时间、序号);列表分区擅长处理「离散」的维度(枚举)。比如一张全国订单表,如果业务查询几乎总是带上 region 条件(WHERE region = 'east'),那么把 region 做成列表分区键,就能让这些查询只扫描一个分区。
三种声明式分区方法的对照如下:
| 方法 | 边界写法 | 分区键列数上限 | 键值是否连续 | 典型列 |
|---|---|---|---|---|
RANGE | FROM (...) TO (...) | 多列(复合) | 连续区间 | 时间、ID |
LIST | IN (...) | 单列或单表达式 | 离散枚举 | 地区、租户、状态 |
HASH | WITH (MODULUS, REMAINDER) | 多列(复合) | 打散无界 | 高基数列 |
列表分区只能有一个分区键列
官方对 PARTITION BY 的说明中有一句关键限制:RANGE 与 HASH 的分区键可以包含多列(最多 32 列),而 LIST 的分区键必须由单个列或单个表达式构成。
-- 错误!列表分区不允许两列分区键
CREATE TABLE t (region text, channel text) PARTITION BY LIST (region, channel);ERROR: cannot use "list" partition strategy with more than one column如果确实需要「地区 + 渠道」两维穷举,应改用多级分区(先按 region LIST,再按 channel LIST),或用一个生成列把两列拼成一个表达式。
边界语法:IN (...)
列表分区的边界形式在 partition_bound_spec 中定义为:
IN ( partition_bound_expr [, ...] )partition_bound_expr 必须是不引用任何列的自由表达式(变量无关表达式),其数据类型必须与分区键列一致。它只在该分区创建那一刻求值一次,所以连 CURRENT_TIMESTAMP 这类易变表达式都允许(但不推荐)。
-- 每个地区一个分区
CREATE TABLE orders_east PARTITION OF orders FOR VALUES IN ('east');
CREATE TABLE orders_west PARTITION OF orders FOR VALUES IN ('west');
CREATE TABLE orders_north PARTITION OF orders FOR VALUES IN ('north', 'south');注意一个分区的 IN (...) 里可以写多个值,这些值共享同一个物理分区,这与范围分区的「一段区间」是对应的。
边界必须互不重叠
与范围分区一样,列表分区的边界集合之间不允许重叠。若某个值已被 orders_east 占用,再建一个包含 'east' 的分区就会失败:
ERROR: partition "orders_east2" would overlap with existing partition "orders_east"因此,每个枚举值在整个分区表里至多归属于一个分区(或者归属于 DEFAULT 分区这一处兜底)。
DEFAULT 分区
DEFAULT 是一个独立于 IN (...) 的关键字形式,它接收所有「未被任何显式分区覆盖」的值:
CREATE TABLE orders_default PARTITION OF orders DEFAULT;对于列表分区,DEFAULT 的价值比范围分区更微妙:当业务新增了一个枚举值(例如新开了 'central' 大区),如果它还没有专属分区,那么所有 'central' 的行都会进入 DEFAULT 分区。这既是一道保险,也是一个需要监控的隐患(见下文「详细说明」)。
NULL 的处理:列表分区独有的能力
这是列表分区相对范围分区的一个特殊之处:列表分区允许显式声明一个接收 NULL 的分区,写法就是在 IN 里列出 NULL:
CREATE TABLE orders_unknown PARTITION OF orders FOR VALUES IN (NULL);官方原文:When creating a list partition, NULL can be specified to signify that the partition allows the partition key column to be null. However, there cannot be more than one such list partition for a given parent table. 也就是说:
| 规则 | 说明 |
|---|---|
可以有一个 IN (NULL) 分区 | 专门接收分区键为 NULL 的行 |
| 最多只能有一个 | 多个会因「边界重叠」而报错 |
RANGE 不允许写 NULL | 范围分区的边界不能是 NULL |
与「枚举列 + 普通索引」的取舍
在决定用列表分区之前,先问一个问题:这张表真的需要按这个维度切分,还是只需要在这个列上建个索引?
| 诉求 | 枚举列 + B-tree 索引 | 列表分区 |
|---|---|---|
| 单值查询快 | 可以(索引扫描) | 可以(裁剪 + 小表扫描) |
| 大批量删除某个值的数据 | 需要 DELETE(慢、产生膨胀) | DETACH + DROP(秒级) |
| 按值迁移到不同存储 | 不支持 | 支持(各分区可指定 TABLESPACE) |
| 小表(几百万行以内) | 推荐 | 多半不需要 |
| 值数量很少(2~3 个)且稳定 | 索引足够 | 需谨慎(分区收益有限) |
结论:列表分区的核心收益在「按值归档 / 快速下线数据」与「按值隔离存储」,而不是单纯提速查询。
语法与用法
创建父表
CREATE TABLE [ IF NOT EXISTS ] table_name (
column_name data_type [ column_constraint [ ... ] ]
[, ... ]
)
PARTITION BY LIST ( { column_name | ( expression ) } [ COLLATE collation ] [ opclass ] )创建分区
CREATE TABLE [ IF NOT EXISTS ] table_name
PARTITION OF parent_table
{ FOR VALUES IN ( partition_bound_expr [, ...] ) | DEFAULT }
[ PARTITION BY { RANGE | LIST | HASH } ( ... ) ] -- 可选:继续子分区
[ TABLESPACE tablespace_name ]完整示例
-- 父表:按区域(region)做列表分区
CREATE TABLE orders (
order_id bigint GENERATED BY DEFAULT AS IDENTITY,
region text NOT NULL,
customer text NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
-- 主键必须包含分区键 region,否则无法创建
PRIMARY KEY (order_id, region)
) PARTITION BY LIST (region);
-- 各区域分区
CREATE TABLE orders_east PARTITION OF orders FOR VALUES IN ('east');
CREATE TABLE orders_west PARTITION OF orders FOR VALUES IN ('west');
CREATE TABLE orders_north PARTITION OF orders FOR VALUES IN ('north', 'south');
-- 兜底分区:接收尚未建立专属分区的新区域值
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
-- 在父表上建索引,会自动下推到所有现有及未来分区
CREATE INDEX idx_orders_created_at ON orders (created_at);查看结构
postgres=# \d+ orders
Partitioned table "public.orders"
Column | Type | Collation | Nullable | Default
------------+--------------------------+-----------+----------+-------------------------------
order_id | bigint | | not null | generated by default as identity
region | text | | not null |
customer | text | | not null |
amount | numeric(12,2) | | not null |
created_at | timestamp with time zone | | not null | now()
Partition key: LIST (region)
Indexes:
"orders_pkey" PRIMARY KEY, btree (order_id, region)
"idx_orders_created_at" btree (created_at)
Number of partitions: 4 (Use \d+ to list them.)postgres=# \d+ orders
...
Partitions:
orders_default FOR VALUES IN (DEFAULT)
orders_east FOR VALUES IN ('east')
orders_north FOR VALUES IN ('north', 'south')
orders_west FOR VALUES IN ('west')查询分区边界
每个分区的边界以「内部表达式树」的形式存放在 pg_class.relpartbound 中,需要配合 pg_get_expr() 转成可读文本:
SELECT c.relname AS partition,
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; partition | bound
----------------+---------------------------------
orders_default | DEFAULT
orders_east | FOR VALUES IN ('east')
orders_north | FOR VALUES IN ('north', 'south')
orders_west | FOR VALUES IN ('west')也可以直接查 pg_partitioned_table.partstrat 确认分区策略类型(l = list):
SELECT c.relname,
pt.partstrat, -- r=range, l=list, h=hash
pg_get_partkeydef(c.oid) AS partkey
FROM pg_partitioned_table pt
JOIN pg_class c ON c.oid = pt.partrelid; relname | partstrat | partkey
---------+-----------+---------
orders | l | LIST (region)详细说明
枚举值的增删:分区是「只读枚举」的代价
列表分区把「允许的枚举值集合」钉死在 DDL 里。这意味着每当业务新增一个枚举值,运维都要多一步动作:
-- 新增「central」大区:先 DETACH 出 DEFAULT(如果它已含有 central 的数据,见下文)
ALTER TABLE orders DETACH PARTITION orders_default;
-- 建新分区
CREATE TABLE orders_central PARTITION OF orders FOR VALUES IN ('central');
-- 把原 DEFAULT 里属于 central 的数据搬回去(若有)
INSERT INTO orders_central SELECT * FROM orders_default WHERE region = 'central';
DELETE FROM orders_default WHERE region = 'central';
-- 重新挂回 DEFAULT 分区
ALTER TABLE orders ATTACH PARTITION orders_default DEFAULT;这一串操作的复杂度,是列表分区最大的运维负担。生产上通常用配置表 + 定时任务自动化(见「实战示例」),避免依赖人工记忆。
多租户(multi-tenant)场景
SaaS 系统里,列表分区最常见的用法是按租户隔离数据。对于有几十到几百个租户、且各租户数据量差异悬殊的场景,可以「大租户独立分区、小租户共享分区」:
CREATE TABLE events (
tenant_id int NOT NULL,
event_id bigint NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (event_id, tenant_id)
) PARTITION BY LIST (tenant_id);
-- 头部大客户各占一个分区,便于单独备份 / 迁移 / 下线
CREATE TABLE events_t1001 PARTITION OF events FOR VALUES IN (1001);
CREATE TABLE events_t1002 PARTITION OF events FOR VALUES IN (1002);
-- 长尾租户共用一个分区
CREATE TABLE events_small PARTITION OF events FOR VALUES IN (2001, 2002, 2003, 2004, 2005, 2006);
-- 未来新签约的租户先落这里,签约后再迁出
CREATE TABLE events_default PARTITION OF events DEFAULT;一个重要提醒:如果每个租户一个分区,租户数几千上万时会直接撞上「分区数量爆炸」(规划时间、pg_class 膨胀、锁传播都会恶化)。这类场景更适合用 HASH 分区(按 tenant_id 模数打散),而不是把每个租户变成独立分区。
地区 / 国家 / 语言等静态维度
对于取值集合基本固定的维度(洲、大区、语言、币种),列表分区几乎没有维护成本:
CREATE TABLE sales (
sale_id bigint GENERATED BY DEFAULT AS IDENTITY,
continent text NOT NULL,
amount numeric(12,2) NOT NULL,
sold_at date NOT NULL,
PRIMARY KEY (sale_id, continent)
) PARTITION BY LIST (continent);
CREATE TABLE sales_asia PARTITION OF sales FOR VALUES IN ('AS');
CREATE TABLE sales_europe PARTITION OF sales FOR VALUES IN ('EU');
CREATE TABLE sales_amer PARTITION OF sales FOR VALUES IN ('NA', 'SA');
CREATE TABLE sales_other PARTITION OF sales DEFAULT;这类维度的特点是「值集合封闭」,所以列表分区的缺点(枚举固定)在这里基本不构成问题。
NULL 的处理与「NULL 分区」
分区键为 NULL 的行不会自动归入某个分区,除非:
- 建了
DEFAULT分区(NULL会被DEFAULT接收,前提是没有显式的IN (NULL)分区); - 或建了显式的
IN (NULL)分区。
-- 显式把 NULL 收进独立分区
CREATE TABLE orders_unknown PARTITION OF orders FOR VALUES IN (NULL);注意:即使父表列上有 NOT NULL 约束,也仍可声明 IN (NULL) 分区——它等价于一个永远为空的分区(除非约束被放宽)。反过来说,如果分区键列可空且既没有 NULL 分区也没有 DEFAULT 分区,插入 NULL 就会直接报错:
ERROR: no partition of relation "orders" found for row
DETAIL: Partition key of the failing row contains (region) = (null).DEFAULT 分区:保险还是陷阱
DEFAULT 分区对列表分区而言是双刃剑:
| 角度 | 说明 |
|---|---|
| 保护 | 新枚举值入库不会中断业务 |
| 风险 | 数据「悄悄」堆积在 DEFAULT 里,可能撑爆唯一一个大分区 |
| 风险 | ATTACH PARTITION 新分区时,若 DEFAULT 中含冲突行会直接报错 |
第 3 条的具体表现:
-- DEFAULT 里已经有 central 的行,直接建 central 分区会失败
CREATE TABLE orders_central PARTITION OF orders FOR VALUES IN ('central');ERROR: updated partition constraint for default partition "orders_default" would be violated by some row处理方式:先 DETACH 出 DEFAULT,搬走冲突数据,再重建 DEFAULT;或者在被 ATTACH 的目标表上预加 CHECK 约束以跳过对 DEFAULT 的全表扫描与校验(详见 创建与附加分区)。
单列限制的绕行:生成列与多级分区
当需要「按两列的组合值枚举分区」时,有两种正规做法。
做法一:用生成列把组合压成单列。
CREATE TABLE tickets (
ticket_id bigint NOT NULL,
region text NOT NULL,
channel text NOT NULL,
-- 生成列:region || ':' || channel,作为唯一分区键
region_chan text GENERATED ALWAYS AS (region || ':' || channel) STORED
) PARTITION BY LIST (region_chan);
CREATE TABLE tickets_east_web PARTITION OF tickets FOR VALUES IN ('east:web');
CREATE TABLE tickets_east_app PARTITION OF tickets FOR VALUES IN ('east:app');注意生成列必须是 STORED(PostgreSQL 18 中 VIRTUAL 生成列也已支持,但作为分区键使用仍以 STORED 最为稳妥),且生成表达式必须是 IMMUTABLE 的。
做法二:多级分区(先按 region LIST,再按 channel LIST)。
CREATE TABLE tickets (ticket_id bigint, region text, channel text)
PARTITION BY LIST (region);
CREATE TABLE tickets_east PARTITION OF tickets FOR VALUES IN ('east')
PARTITION BY LIST (channel);
CREATE TABLE tickets_east_web PARTITION OF tickets_east FOR VALUES IN ('web');
CREATE TABLE tickets_east_app PARTITION OF tickets_east FOR VALUES IN ('app');见 多级分区(子分区)。
与范围分区混合使用
列表与范围互不冲突,常见组合是「先按大区 LIST,再按时间 RANGE」——外层用于数据隔离与归档,内层用于时间裁剪与轮转:
CREATE TABLE logs (region text, logdate date, msg text)
PARTITION BY LIST (region);
CREATE TABLE logs_east PARTITION OF logs FOR VALUES IN ('east')
PARTITION BY RANGE (logdate);
CREATE TABLE logs_east_2026_01 PARTITION OF logs_east
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');用 ATTACH / DETACH 动态调整枚举值
列表分区的枚举值调整,本质上是对分区集合的增删:
-- 把已有表挂成 'north' 的分区(须先保证表中数据满足边界)
ALTER TABLE orders ATTACH PARTITION orders_north FOR VALUES IN ('north');
-- 摘除分区(PG 14+ 可加 CONCURRENTLY 降低锁级别)
ALTER TABLE orders DETACH PARTITION orders_north;
ALTER TABLE orders DETACH PARTITION orders_north CONCURRENTLY; -- PostgreSQL 14+ATTACH 时会校验表内数据是否满足 IN ('north'),若表大则会全表扫描;可预先加 CHECK 约束跳过扫描:
ALTER TABLE orders_north ADD CONSTRAINT ck_north CHECK (region = 'north');
ALTER TABLE orders ATTACH PARTITION orders_north FOR VALUES IN ('north');
ALTER TABLE orders_north DROP CONSTRAINT ck_north;列表分区的裁剪行为
只要查询条件是分区键的等值或 IN 列表,裁剪就能精确命中:
EXPLAIN (COSTS OFF) SELECT * FROM orders WHERE region = 'east'; QUERY PLAN
------------------------------------------
Seq Scan on orders_east ordersEXPLAIN (COSTS OFF) SELECT * FROM orders WHERE region IN ('north', 'south'); QUERY PLAN
------------------------------------------
Seq Scan on orders_north orders反之,任何把分区键「包在函数里」或做隐式类型转换的写法都会破坏裁剪:
-- 不推荐:函数包裹分区键,裁剪失效,需扫描全部分区
SELECT * FROM orders WHERE upper(region) = 'EAST';
-- 不推荐:类型不一致导致的隐式转换也可能影响裁剪
SELECT * FROM orders WHERE region = 1;列表分区键的选择标准
选择列表分区键时,可以对照下表自查:
| 标准 | 说明 |
|---|---|
| 基数适中 | 理想是「几十到几百个值」,太少(2~3 个)分区收益有限,太多则维护困难 |
| 值集合稳定 | 枚举值不常变化;若天天新增,维护成本会压垮收益 |
| 查询常带该列等值条件 | 否则裁剪拿不到收益,只是白白增加管理复杂度 |
| 能承载归档需求 | 能通过 DETACH/DROP 一次性下线某类数据 |
| 单分区大小可控 | 避免某个值数据量占比过高,导致「一个大分区 + 一堆小分区」 |
实战示例
本例演示一个多维度的订单表,按「区域」列表分区,并按「时间」做二级范围分区,最后演练新增区域值的完整流程。
第 1 步:创建父表与一级(LIST)分区
CREATE TABLE orders (
order_id bigint GENERATED BY DEFAULT AS IDENTITY,
region text NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (order_id, region)
) PARTITION BY LIST (region);
CREATE TABLE orders_east PARTITION OF orders FOR VALUES IN ('east') PARTITION BY RANGE (created_at);
CREATE TABLE orders_west PARTITION OF orders FOR VALUES IN ('west') PARTITION BY RANGE (created_at);
CREATE TABLE orders_default PARTITION OF orders DEFAULT;第 2 步:为 east / west 预建月度子分区
SELECT format(
'CREATE TABLE orders_%s_%s PARTITION OF orders_%s FOR VALUES FROM (%L) TO (%L);',
r, to_char(m, 'YYYY_MM'), r, m::date, (m + interval '1 month')::date
) AS ddl
FROM (VALUES ('east'), ('west')) AS regions(r)
CROSS JOIN generate_series(
date_trunc('month', DATE '2026-01-01'),
date_trunc('month', DATE '2026-06-01'),
interval '1 month') AS m; ddl
------------------------------------------------------------------------
CREATE TABLE orders_east_2026_01 PARTITION OF orders_east FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_east_2026_02 PARTITION OF orders_east FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
...第 3 步:写入数据并观察路由
INSERT INTO orders (region, amount, created_at)
SELECT (ARRAY['east','west','north'])[1 + (i % 3)],
(random() * 1000)::numeric(12,2),
DATE '2026-01-01' + (i % 150) * interval '1 day'
FROM generate_series(1, 30000) AS i;
-- 用 tableoid 观察每行实际落在哪个物理分区
SELECT tableoid::regclass AS partition, region, count(*)
FROM orders
GROUP BY 1, 2
ORDER BY 1; partition | region | count
--------------------+--------+-------
orders_default | north | 10000
orders_east_2026_01| east | 1550
orders_east_2026_02| east | 1450
orders_west_2026_01| west | 1520
...可以看到:east/west 的数据被路由到各自的月度子分区,而 north 因为没有专属分区,全部落进了 DEFAULT。
第 4 步:为 north 建专属分区
DEFAULT 里已经有 north 的数据,因此不能直接建:
CREATE TABLE orders_north PARTITION OF orders FOR VALUES IN ('north');ERROR: updated partition constraint for default partition "orders_default" would be violated by some row正确流程是「摘除 DEFAULT → 建分区 → 搬数据 → 挂回 DEFAULT」:
BEGIN;
-- 1) 摘除 DEFAULT(ACCESS EXCLUSIVE 锁父表)
ALTER TABLE orders DETACH PARTITION orders_default;
-- 2) 建 north 分区
CREATE TABLE orders_north PARTITION OF orders FOR VALUES IN ('north') PARTITION BY RANGE (created_at);
-- 3) 把 DEFAULT 中属于 north 的数据搬到 north 分区(这里 north 尚无子分区,
-- 为演示方便先建一个宽范围子分区承载)
CREATE TABLE orders_north_catchall PARTITION OF orders_north
FOR VALUES FROM (MINVALUE) TO (MAXVALUE);
INSERT INTO orders_north SELECT * FROM orders_default WHERE region = 'north';
DELETE FROM orders_default WHERE region = 'north';
-- 4) DEFAULT 现在只含「未来新区域」,重新挂回
ALTER TABLE orders ATTACH PARTITION orders_default DEFAULT;
COMMIT;第 5 步:验证
SELECT tableoid::regclass AS partition, count(*)
FROM orders GROUP BY 1 ORDER BY 1; partition | count
------------------------+-------
orders_east_2026_01 | 1550
orders_east_2026_02 | 1450
orders_north_catchall | 10000
orders_west_2026_01 | 1520
...
(orders_default 已清空,不再出现在结果中)第 6 步:裁剪验证
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF)
SELECT count(*) FROM orders WHERE region = 'north'; QUERY PLAN
------------------------------------------------------------
Aggregate
-> Seq Scan on orders_north_catchall orders只扫描了 orders_north 这一支,其余分区全部被裁剪。
注意事项
- 错误!列表分区不支持多列分区键。 需要多维度枚举时,改用生成列或多级分区。
-- 错误!
CREATE TABLE t (a text, b text) PARTITION BY LIST (a, b);
-- ERROR: cannot use "list" partition strategy with more than one column- 错误!
IN (NULL)分区最多只能有一个。 多个会被判定为边界重叠。
CREATE TABLE t1 PARTITION OF t FOR VALUES IN (NULL);
CREATE TABLE t2 PARTITION OF t FOR VALUES IN (NULL);
-- ERROR: partition "t2" would overlap with existing partition "t1"- 错误!向
DEFAULT分区中已有数据的父表直接新建分区会失败。 必须先DETACH出DEFAULT并搬走冲突数据。
ERROR: updated partition constraint for default partition "..." would be violated by some row- 不推荐用
upper()、lower()、cast()等包裹分区键。 会破坏裁剪,导致全分区扫描。
-- 不推荐
SELECT * FROM orders WHERE lower(region) = 'east';
-- 推荐
SELECT * FROM orders WHERE region = 'east';- 不推荐把分区键做隐式类型转换。
WHERE region = 1这类写法会让text列与整数比较,可能触发异常或无法裁剪。
-- 推荐:显式使用与列类型一致的常量
SELECT * FROM orders WHERE region = '1';- 错误!分区键列可空却既无
NULL分区也无DEFAULT分区时,插入NULL会报错。
ERROR: no partition of relation "orders" found for row
DETAIL: Partition key of the failing row contains (region) = (null).- 不推荐把「高基数列」当列表分区键。 例如用户 ID、订单号动辄百万取值,会导致分区数量爆炸,规划与维护成本急剧上升。
-- 不推荐:user_id 有上百万个取值
CREATE TABLE t (user_id bigint) PARTITION BY LIST (user_id);- 不推荐让各分区数据量严重倾斜。 「一个大分区 + 一堆空分区」既没有裁剪收益,又增加了管理负担。
-- 不推荐:99% 的数据都在 orders_default 里- 错误!列表分区的父表主键/唯一约束必须包含分区键。 否则无法创建。
-- 错误!
CREATE TABLE orders2 (order_id bigint PRIMARY KEY, region text) PARTITION BY LIST (region);
-- ERROR: unique constraint on partitioned table must include all partitioning columns最佳实践
- 列表分区键优先选「值集合封闭且基数适中」的静态维度。 地区、大区、币种、语言是理想选择;用户 ID、订单号不是。
) PARTITION BY LIST (region);- 永远为列表分区表建一个
DEFAULT分区作为兜底,并每日监控其行数。 一旦发现DEFAULT持续增长,说明有枚举值没有被正确分区。
SELECT count(*) AS default_rows FROM orders_default;- 用配置表 + 定时任务自动维护枚举值到分区的映射,把「新增区域」变成数据行而非 DDL 改动。
CREATE TABLE partition_map (region text PRIMARY KEY, partition_name text NOT NULL, created_at timestamptz DEFAULT now());
INSERT INTO partition_map (region, partition_name) VALUES ('central', 'orders_central') ON CONFLICT DO NOTHING;- 批量生成建分区 DDL,避免手工拼写枚举值出错。
SELECT format('CREATE TABLE orders_%s PARTITION OF orders FOR VALUES IN (%L);',
p.partition_name, p.region)
FROM partition_map p
WHERE NOT EXISTS (SELECT 1 FROM pg_class c WHERE c.relname = 'orders_' || p.partition_name);- 给每个分区都建上必要的索引(在父表上
CREATE INDEX会自动下推),确保单分区查询不退化。
CREATE INDEX idx_orders_created_at ON orders (created_at, region);- 归档下线某类数据时用
DETACH+DROP,而不是DELETE。
ALTER TABLE orders DETACH PARTITION orders_east; -- 如需低锁:CONCURRENTLY(PG 14+)
DROP TABLE orders_east;- 新增枚举值前,先确认目标分区名在该父表下唯一,并检查
DEFAULT中是否已有该值的数据。
SELECT count(*) FROM orders_default WHERE region = 'central';- 给
DEFAULT分区设置行数增长告警,例如「连续 24 小时新增超过 1000 行」触发人工介入。
SELECT pg_size_pretty(pg_total_relation_size('orders_default')) AS size;- 在应用层约束枚举取值(外键或
CHECK),保证分区键的取值范围受控。
ALTER TABLE orders ADD CONSTRAINT ck_region CHECK (region IN ('east','west','north','central'));下一节
继续学习:哈希分区(HASH)