跳到主要内容

表分区

表分区(table partitioning)把一张逻辑大表拆成多张物理分区。应用通常查询父分区表,PostgreSQL 根据分区键把数据路由到正确分区,并在可能时只扫描相关分区。

分区主要解决超大表的管理与访问问题,例如快速归档旧数据、按时间维护索引、减少部分查询扫描范围。它不会自动让所有查询变快,也不能替代合理索引。

本文以 PostgreSQL 声明式分区为主。

何时考虑分区​

分区通常在以下情况有价值:

  • 表已大到维护、归档或删除历史数据成本明显;
  • 查询大多带有稳定的时间、租户或业务范围条件;
  • 可以通过挂载、分离分区快速导入或移出整批数据;
  • 单个分区的索引和维护任务更容易控制;
  • 数据生命周期天然按范围切分。

表只有几百万行并不自动意味着需要分区。数据规模、行宽、索引、查询选择性、硬件和维护目标共同决定是否值得。

创建分区表​

按时间范围分区:

CREATE TABLE events (
event_id BIGINT GENERATED ALWAYS AS IDENTITY,
occurred_at TIMESTAMPTZ NOT NULL,
tenant_id BIGINT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL
) PARTITION BY RANGE (occurred_at);

父分区表定义逻辑结构,实际数据存放在叶子分区中。

创建月分区:

CREATE TABLE events_2026_09
PARTITION OF events
FOR VALUES FROM ('2026-09-01 00:00:00+00')
TO ('2026-10-01 00:00:00+00');

范围采用下界包含、上界不包含的半开区间。相邻分区可以无缝衔接,某个时间点不会同时属于两个分区。

三种分区策略​

范围分区 RANGE​

适合时间、递增编号或连续范围:

PARTITION BY RANGE (occurred_at)

常用按日、月、季度或年划分。粒度过细会产生大量对象,过粗则降低维护收益。

列表分区 LIST​

适合离散集合:

CREATE TABLE customers (
customer_id BIGINT NOT NULL,
region_code TEXT NOT NULL
) PARTITION BY LIST (region_code);

CREATE TABLE customers_east
PARTITION OF customers
FOR VALUES IN ('CN-SH', 'CN-JS', 'CN-ZJ');

当区域集合经常变化或数量很大时,应评估维护成本。

哈希分区 HASH​

适合把数据较均匀地散布到固定数量分区:

CREATE TABLE sessions (
session_id UUID NOT NULL,
user_id BIGINT NOT NULL
) PARTITION BY HASH (user_id);

CREATE TABLE sessions_p0
PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 0);

还需创建余数 1、2、3 的分区。哈希分区便于均匀分布,但不像时间范围那样容易整体归档旧数据,改变分区数量也需要重新分布数据。

默认分区​

未匹配任何明确分区的行会插入失败。可以创建默认分区:

CREATE TABLE events_default
PARTITION OF events DEFAULT;

默认分区能避免因忘记提前创建下期分区而中断写入,但也可能掩盖路由配置错误。应监控默认分区行数,并及时把合法数据迁移到正确分区。

新增明确分区时,PostgreSQL 可能需要扫描默认分区以确认其中没有属于新区间的数据。可通过匹配的检查约束和受控迁移减少验证成本。

数据路由​

向父表插入:

INSERT INTO events (
occurred_at,
tenant_id,
event_type,
payload
) VALUES (
'2026-09-16 08:00:00+00',
42,
'login',
'{}'
);

PostgreSQL 根据 occurred_at 路由到 events_2026_09。不要让应用直接选择月表名,否则会复制路由逻辑并增加注入和迁移风险。

更新分区键后,如果新值属于其他分区,PostgreSQL 可把行移动到目标分区。并发更新时仍可能出现冲突,应避免无必要地修改分区键。

分区裁剪​

查询条件能推导出分区范围时,优化器可以跳过无关分区:

SELECT event_type, count(*)
FROM events
WHERE occurred_at >= '2026-09-01 00:00:00+00'
AND occurred_at < '2026-10-01 00:00:00+00'
GROUP BY event_type;

使用 EXPLAIN 检查实际扫描了哪些分区:

EXPLAIN
SELECT *
FROM events
WHERE occurred_at >= '2026-09-15 00:00:00+00';

裁剪依赖查询条件能与分区边界对应。对分区键包裹复杂函数、隐式类型转换或缺少分区条件,可能导致扫描更多分区。

分区裁剪与索引是两回事:裁剪决定排除哪些表,索引决定在剩余分区中如何找行。

索引​

在父分区表上创建索引:

CREATE INDEX idx_events_tenant_time
ON events (tenant_id, occurred_at);

PostgreSQL 会管理对应的分区索引。每个分区也可以拥有额外的本地索引,以适应冷热数据的不同查询模式。

在繁忙大表上建立分区索引需要规划并发与锁。某些并发建索引能力对分区父表有限制,常见做法是逐个在分区上并发创建索引,再附加到父级索引;具体流程应按目标版本文档演练。

主键与唯一约束​

PostgreSQL 的分区表主键或唯一约束通常必须包含全部分区键列,并且分区键不能以不受支持的表达式形式绕开这一要求:

CREATE TABLE orders (
order_id BIGINT NOT NULL,
ordered_at DATE NOT NULL,
customer_id BIGINT NOT NULL,
CONSTRAINT pk_orders PRIMARY KEY (ordered_at, order_id)
) PARTITION BY RANGE (ordered_at);

原因是各叶子分区使用独立物理索引,数据库需要从键值判断可能发生冲突的分区。

如果业务要求 order_id 跨所有时间分区全局唯一,可考虑:

  • 使用不会重复的外部生成方案,同时接受数据库内缺少单一全局唯一约束的边界;
  • 调整主键与分区键;
  • 使用未分区的键注册表;
  • 重新评估是否需要按该键分区。

不要仅因为概率很低就忽略数据模型对唯一性的承诺。

外键与约束​

现代 PostgreSQL 支持分区表参与多种外键关系,但版本演进较快,复杂引用应在目标版本验证创建、删除、挂载分区和性能行为。

父表上的 CHECK、NOT NULL 和其他支持的约束会影响分区。分区自身还隐含边界约束。单独查询或直接写入叶子分区时,仍应确保权限和约束符合整体设计。

挂载已有表​

可以先创建普通表并加载、校验数据,再挂载为分区:

CREATE TABLE events_2026_10
(LIKE events INCLUDING DEFAULTS INCLUDING CONSTRAINTS);

先添加与目标边界一致的检查约束,再挂载:

ALTER TABLE events
ATTACH PARTITION events_2026_10
FOR VALUES FROM ('2026-10-01 00:00:00+00')
TO ('2026-11-01 00:00:00+00');

精确且已验证的检查约束有机会避免挂载时扫描整表。操作仍需评估锁和并发影响。

分离与归档​

分离旧分区:

ALTER TABLE events
DETACH PARTITION events_2025_09;

分离后它成为独立表,可备份、导出或删除。部分 PostgreSQL 版本支持更适合降低并发阻塞的分离选项,使用时应查阅目标版本。

相比逐行执行大规模 DELETE,直接分离或删除整个旧分区通常更快、产生的行级清理工作更少。但仍要处理外键、备份、归档确认和磁盘回收。

子分区​

分区本身还可以继续分区,例如先按月份范围,再按租户哈希:

CREATE TABLE events_2026_09
PARTITION OF events
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01')
PARTITION BY HASH (tenant_id);

多级分区会增加对象数量、计划时间和运维复杂度。只有单层方案不能满足明确目标时才考虑。

自动化维护​

时间分区需要持续创建未来分区、归档旧分区。自动化任务应:

  • 提前创建足够周期的分区;
  • 使用统一命名和边界时区;
  • 检查分区是否重叠或存在空档;
  • 同步索引、权限、触发器和监控;
  • 监控默认分区和插入失败;
  • 归档前验证数据完整性与备份可恢复性。

边界时间应统一使用明确时区。按本地日历分区时,要特别处理夏令时与时区规则变化。

常见误区​

分区后查询一定更快​

缺少分区过滤的查询可能扫描全部分区,规划开销甚至更高。分区首先是数据管理工具。

分区可以替代索引​

裁剪只排除无关分区,单个分区内部仍可能需要索引。

分区越细越好​

分区过多会增加系统目录、规划、迁移和监控成本。粒度要匹配数据量和生命周期操作。

应用应该直接读写具体分区​

通常应访问父表,让数据库路由。直接操作叶子分区会让应用耦合物理布局。

分区设计检查清单​

  1. 分区要解决的是查询性能、归档还是维护窗口问题;
  2. 大多数关键查询是否包含可裁剪的分区键条件;
  3. RANGE、LIST 或 HASH 哪一种匹配数据生命周期;
  4. 分区粒度是否会产生过多对象;
  5. 主键与唯一约束能否包含全部分区键;
  6. 每个分区是否需要索引、权限、触发器和 RLS;
  7. 默认分区是否被监控;
  8. 是否自动创建未来分区并归档旧分区;
  9. 挂载、分离和索引维护是否在目标版本演练过;
  10. 真实查询是否通过 EXPLAIN 验证了裁剪效果。

好的分区方案从数据生命周期和访问模式出发。先明确要减少哪一种成本,再选择键、粒度和维护流程,比分完表后期待优化器自动解决一切更可靠。