表分区
表分区(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);
多级分区会增加对象数量、计划时间和运维复杂度。只有单层方案不能满足明确目标时才考虑。
自动化维护
时间分区需要持续创建未来分区、归档旧分区。自动化任务应:
- 提前创建足够周期的分区;
- 使用统一命名和边界时区;
- 检查分区是否重叠或存在空档;
- 同步索引、权限、触发器和监控;
- 监控默认分区和插入失败;
- 归档前验证数据完整性与备份可恢复性。
边界时间应统一使用明确时区。按本地日历分区时,要特别处理夏令时与时区规则变化。
常见误区
分区后查询一定更快
缺少分区过滤的查询可能扫描全部分区,规划开销甚至更高。分区首先是数据管理工具。
分区可以替代索引
裁剪只排除无关分区,单个分区内部仍可能需要索引。
分区越细越好
分区过多会增加系统目录、规划、迁移和监控成本。粒度要匹配数据量和生命周期操作。
应用应该直接读写具体分区
通常应访问父表,让数据库路由。直接操作叶子分区会让应用耦合物理布局。
分区设计检查清单
- 分区要解决的是查询性能、归档还是维护窗口问题;
- 大多数关键查询是否包含可裁剪的分区键条件;
RANGE、LIST或HASH哪一种匹配数据生命周期;- 分区粒度是否会产生过多对象;
- 主键与唯一约束能否包含全部分区键;
- 每个分区是否需要索引、权限、触发器和 RLS;
- 默认分区是否被监控;
- 是否自动创建未来分区并归档旧分区;
- 挂载、分离和索引维护是否在目标版本演练过;
- 真实查询是否通过
EXPLAIN验证了裁剪效果。
好的分区方案从数据生命周期和访问模式出发。先明确要减少哪一种成本,再选择键、粒度和维护流程,比分完表后期待优化器自动解决一切更可靠。