Skip to content

列表分区(LIST) ​

列表分区(List Partitioning)把分区键的取值逐个枚举到指定的分区里:FOR VALUES IN ('a','b') 表示这个分区接收所有分区键等于 a 或 b 的行。它没有区间概念,也不做取模计算,是三种声明式分区中语义最直白的一种——「这个分区装哪些值」一眼就能从 DDL 里读出来。

列表分区的典型场景是离散维度:地区(region)、国家代码、租户(tenant)、业务线、状态枚举。这些列的取值是有限、枚举、可数的,天然适合按值归档,而不是按时间轮转。

前置知识

阅读本节前,建议先了解:声明式分区概述、范围分区(RANGE)

基础概念 ​

列表分区在解决什么问题 ​

范围分区擅长处理「连续」的维度(时间、序号);列表分区擅长处理「离散」的维度(枚举)。比如一张全国订单表,如果业务查询几乎总是带上 region 条件(WHERE region = 'east'),那么把 region 做成列表分区键,就能让这些查询只扫描一个分区。

三种声明式分区方法的对照如下:

方法边界写法分区键列数上限键值是否连续典型列
RANGEFROM (...) TO (...)多列(复合)连续区间时间、ID
LISTIN (...)单列或单表达式离散枚举地区、租户、状态
HASHWITH (MODULUS, REMAINDER)多列(复合)打散无界高基数列

列表分区只能有一个分区键列

官方对 PARTITION BY 的说明中有一句关键限制:RANGE 与 HASH 的分区键可以包含多列(最多 32 列),而 LIST 的分区键必须由单个列或单个表达式构成。

sql
-- 错误!列表分区不允许两列分区键
CREATE TABLE t (region text, channel text) PARTITION BY LIST (region, channel);
text
ERROR:  cannot use "list" partition strategy with more than one column

如果确实需要「地区 + 渠道」两维穷举,应改用多级分区(先按 region LIST,再按 channel LIST),或用一个生成列把两列拼成一个表达式。

边界语法:IN (...) ​

列表分区的边界形式在 partition_bound_spec 中定义为:

text
IN ( partition_bound_expr [, ...] )

partition_bound_expr 必须是不引用任何列的自由表达式(变量无关表达式),其数据类型必须与分区键列一致。它只在该分区创建那一刻求值一次,所以连 CURRENT_TIMESTAMP 这类易变表达式都允许(但不推荐)。

sql
-- 每个地区一个分区
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' 的分区就会失败:

text
ERROR:  partition "orders_east2" would overlap with existing partition "orders_east"

因此,每个枚举值在整个分区表里至多归属于一个分区(或者归属于 DEFAULT 分区这一处兜底)。

DEFAULT 分区 ​

DEFAULT 是一个独立于 IN (...) 的关键字形式,它接收所有「未被任何显式分区覆盖」的值:

sql
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

对于列表分区,DEFAULT 的价值比范围分区更微妙:当业务新增了一个枚举值(例如新开了 'central' 大区),如果它还没有专属分区,那么所有 'central' 的行都会进入 DEFAULT 分区。这既是一道保险,也是一个需要监控的隐患(见下文「详细说明」)。

NULL 的处理:列表分区独有的能力 ​

这是列表分区相对范围分区的一个特殊之处:列表分区允许显式声明一个接收 NULL 的分区,写法就是在 IN 里列出 NULL:

sql
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 个)且稳定索引足够需谨慎(分区收益有限)

结论:列表分区的核心收益在「按值归档 / 快速下线数据」与「按值隔离存储」,而不是单纯提速查询。

语法与用法 ​

创建父表 ​

text
CREATE TABLE [ IF NOT EXISTS ] table_name (
    column_name data_type [ column_constraint [ ... ] ]
    [, ... ]
)
PARTITION BY LIST ( { column_name | ( expression ) } [ COLLATE collation ] [ opclass ] )

创建分区 ​

text
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 ]

完整示例 ​

sql
-- 父表:按区域(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);

查看结构 ​

text
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.)
text
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() 转成可读文本:

sql
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;
text
   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):

sql
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;
text
 relname | partstrat | partkey
---------+-----------+---------
 orders  | l         | LIST (region)

详细说明 ​

枚举值的增删:分区是「只读枚举」的代价 ​

列表分区把「允许的枚举值集合」钉死在 DDL 里。这意味着每当业务新增一个枚举值,运维都要多一步动作:

sql
-- 新增「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 系统里,列表分区最常见的用法是按租户隔离数据。对于有几十到几百个租户、且各租户数据量差异悬殊的场景,可以「大租户独立分区、小租户共享分区」:

sql
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 模数打散),而不是把每个租户变成独立分区。

地区 / 国家 / 语言等静态维度 ​

对于取值集合基本固定的维度(洲、大区、语言、币种),列表分区几乎没有维护成本:

sql
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 的行不会自动归入某个分区,除非:

  1. 建了 DEFAULT 分区(NULL 会被 DEFAULT 接收,前提是没有显式的 IN (NULL) 分区);
  2. 或建了显式的 IN (NULL) 分区。
sql
-- 显式把 NULL 收进独立分区
CREATE TABLE orders_unknown PARTITION OF orders FOR VALUES IN (NULL);

注意:即使父表列上有 NOT NULL 约束,也仍可声明 IN (NULL) 分区——它等价于一个永远为空的分区(除非约束被放宽)。反过来说,如果分区键列可空且既没有 NULL 分区也没有 DEFAULT 分区,插入 NULL 就会直接报错:

text
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 条的具体表现:

sql
-- DEFAULT 里已经有 central 的行,直接建 central 分区会失败
CREATE TABLE orders_central PARTITION OF orders FOR VALUES IN ('central');
text
ERROR:  updated partition constraint for default partition "orders_default" would be violated by some row

处理方式:先 DETACH 出 DEFAULT,搬走冲突数据,再重建 DEFAULT;或者在被 ATTACH 的目标表上预加 CHECK 约束以跳过对 DEFAULT 的全表扫描与校验(详见 创建与附加分区)。

单列限制的绕行:生成列与多级分区 ​

当需要「按两列的组合值枚举分区」时,有两种正规做法。

做法一:用生成列把组合压成单列。

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

sql
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」——外层用于数据隔离与归档,内层用于时间裁剪与轮转:

sql
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 动态调整枚举值 ​

列表分区的枚举值调整,本质上是对分区集合的增删:

sql
-- 把已有表挂成 '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 约束跳过扫描:

sql
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 列表,裁剪就能精确命中:

sql
EXPLAIN (COSTS OFF) SELECT * FROM orders WHERE region = 'east';
text
                QUERY PLAN
------------------------------------------
 Seq Scan on orders_east orders
sql
EXPLAIN (COSTS OFF) SELECT * FROM orders WHERE region IN ('north', 'south');
text
                QUERY PLAN
------------------------------------------
 Seq Scan on orders_north orders

反之,任何把分区键「包在函数里」或做隐式类型转换的写法都会破坏裁剪:

sql
-- 不推荐:函数包裹分区键,裁剪失效,需扫描全部分区
SELECT * FROM orders WHERE upper(region) = 'EAST';

-- 不推荐:类型不一致导致的隐式转换也可能影响裁剪
SELECT * FROM orders WHERE region = 1;

列表分区键的选择标准 ​

选择列表分区键时,可以对照下表自查:

标准说明
基数适中理想是「几十到几百个值」,太少(2~3 个)分区收益有限,太多则维护困难
值集合稳定枚举值不常变化;若天天新增,维护成本会压垮收益
查询常带该列等值条件否则裁剪拿不到收益,只是白白增加管理复杂度
能承载归档需求能通过 DETACH/DROP 一次性下线某类数据
单分区大小可控避免某个值数据量占比过高,导致「一个大分区 + 一堆小分区」

实战示例 ​

本例演示一个多维度的订单表,按「区域」列表分区,并按「时间」做二级范围分区,最后演练新增区域值的完整流程。

第 1 步:创建父表与一级(LIST)分区 ​

sql
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 预建月度子分区 ​

sql
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;
text
                                  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 步:写入数据并观察路由 ​

sql
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;
text
     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 的数据,因此不能直接建:

sql
CREATE TABLE orders_north PARTITION OF orders FOR VALUES IN ('north');
text
ERROR:  updated partition constraint for default partition "orders_default" would be violated by some row

正确流程是「摘除 DEFAULT → 建分区 → 搬数据 → 挂回 DEFAULT」:

sql
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 步:验证 ​

sql
SELECT tableoid::regclass AS partition, count(*)
FROM orders GROUP BY 1 ORDER BY 1;
text
       partition        | count
------------------------+-------
 orders_east_2026_01    |  1550
 orders_east_2026_02    |  1450
 orders_north_catchall  | 10000
 orders_west_2026_01    |  1520
 ...
(orders_default 已清空,不再出现在结果中)

第 6 步:裁剪验证 ​

sql
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF)
SELECT count(*) FROM orders WHERE region = 'north';
text
                         QUERY PLAN
------------------------------------------------------------
 Aggregate
   ->  Seq Scan on orders_north_catchall orders

只扫描了 orders_north 这一支,其余分区全部被裁剪。

注意事项 ​

  • 错误!列表分区不支持多列分区键。 需要多维度枚举时,改用生成列或多级分区。
sql
-- 错误!
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) 分区最多只能有一个。 多个会被判定为边界重叠。
sql
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 并搬走冲突数据。
text
ERROR:  updated partition constraint for default partition "..." would be violated by some row
  • 不推荐用 upper()、lower()、cast() 等包裹分区键。 会破坏裁剪,导致全分区扫描。
sql
-- 不推荐
SELECT * FROM orders WHERE lower(region) = 'east';
-- 推荐
SELECT * FROM orders WHERE region = 'east';
  • 不推荐把分区键做隐式类型转换。 WHERE region = 1 这类写法会让 text 列与整数比较,可能触发异常或无法裁剪。
sql
-- 推荐:显式使用与列类型一致的常量
SELECT * FROM orders WHERE region = '1';
  • 错误!分区键列可空却既无 NULL 分区也无 DEFAULT 分区时,插入 NULL 会报错。
text
ERROR:  no partition of relation "orders" found for row
DETAIL:  Partition key of the failing row contains (region) = (null).
  • 不推荐把「高基数列」当列表分区键。 例如用户 ID、订单号动辄百万取值,会导致分区数量爆炸,规划与维护成本急剧上升。
sql
-- 不推荐:user_id 有上百万个取值
CREATE TABLE t (user_id bigint) PARTITION BY LIST (user_id);
  • 不推荐让各分区数据量严重倾斜。 「一个大分区 + 一堆空分区」既没有裁剪收益,又增加了管理负担。
sql
-- 不推荐:99% 的数据都在 orders_default 里
  • 错误!列表分区的父表主键/唯一约束必须包含分区键。 否则无法创建。
sql
-- 错误!
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、订单号不是。
sql
) PARTITION BY LIST (region);
  • 永远为列表分区表建一个 DEFAULT 分区作为兜底,并每日监控其行数。 一旦发现 DEFAULT 持续增长,说明有枚举值没有被正确分区。
sql
SELECT count(*) AS default_rows FROM orders_default;
  • 用配置表 + 定时任务自动维护枚举值到分区的映射,把「新增区域」变成数据行而非 DDL 改动。
sql
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,避免手工拼写枚举值出错。
sql
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 会自动下推),确保单分区查询不退化。
sql
CREATE INDEX idx_orders_created_at ON orders (created_at, region);
  • 归档下线某类数据时用 DETACH + DROP,而不是 DELETE。
sql
ALTER TABLE orders DETACH PARTITION orders_east;  -- 如需低锁:CONCURRENTLY(PG 14+)
DROP TABLE orders_east;
  • 新增枚举值前,先确认目标分区名在该父表下唯一,并检查 DEFAULT 中是否已有该值的数据。
sql
SELECT count(*) FROM orders_default WHERE region = 'central';
  • 给 DEFAULT 分区设置行数增长告警,例如「连续 24 小时新增超过 1000 行」触发人工介入。
sql
SELECT pg_size_pretty(pg_total_relation_size('orders_default')) AS size;
  • 在应用层约束枚举取值(外键或 CHECK),保证分区键的取值范围受控。
sql
ALTER TABLE orders ADD CONSTRAINT ck_region CHECK (region IN ('east','west','north','central'));

下一节

继续学习:哈希分区(HASH)

参考链接 ​