表分区 · 把一张大表拆成很多小表
声明式分区(PG 10+)把一张逻辑大表按 RANGE / LIST / HASH 拆成多张物理子表。查询靠分区裁剪只扫相关分区,老数据 DETACH/DROP 一秒下线(比 DELETE 快几个量级),每个分区还能独立索引、独立 VACUUM。
表分区 · 把一张大表拆成很多小表
索引能让大表查得快,但当单表长到几亿、几十亿行,另一些麻烦会冒出来:VACUUM 越来越慢、删除历史数据要锁很久、单个索引大到装不进内存。这时候的解法不是「查得更巧」,而是把大表物理上拆小——这就是分区。
PostgreSQL 10 起支持声明式分区:你对着一张「父表」写查询,数据库自动把它路由 / 汇总到底下的多张「子表(分区)」。对应用透明,对数据库高效。
三种分区策略
分区键决定「一行该去哪个分区」,有三种策略:
① RANGE(按区间) —— 最典型,按时间或数值区间分。日志、订单按月分:
CREATE TABLE orders (id bigint, placed_at date, amount numeric)
PARTITION BY RANGE (placed_at);
CREATE TABLE orders_2025 PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
-- 2024、2026 各建一个……插入时数据自动落到对应分区,查父表则透明汇总:
orders_2024: 1 行 orders_2025: 2 行 orders_2026: 1 行
orders(父表查询): 4 行 ← 一条 SELECT * FROM orders 汇总所有分区② LIST(按枚举) —— 按离散值分,典型是按地区、按租户:
CREATE TABLE users_cn PARTITION OF users FOR VALUES IN ('CN');
-- users_cn: ['王'] users_us: ['Bob'] users_other: ['Tanaka']③ HASH(按散列) —— 没有天然区间 / 枚举时,按哈希均匀打散,避免热点:
CREATE TABLE events_p0 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 0);
-- 4 个桶,分布相当均匀:p0:259 p1:234 p2:276 p3:231收益一:分区裁剪(只扫该扫的)
分区最直接的性能收益,是分区裁剪(partition pruning):查询条件命中某个分区键时,planner 只扫相关分区,跳过其余全部。
EXPLAIN SELECT * FROM orders WHERE placed_at = '2025-06-20';
-- Seq Scan on orders_2025 ← 只扫 2025 这一个分区,2024/2026 直接跳过一张按月分了三年的表(36 个分区),查某一天的数据只碰 1 个分区——相当于把扫描范围缩到 1/36。裁剪能在计划期(条件是常量)或执行期(条件是参数 / 来自 JOIN)发生。
收益二:老数据一秒下线
这是分区最爽的地方。要下线整月 / 整年的老数据,不用 DELETE(逐行删 + 产生大量死元组 + 事后 VACUUM,慢且涨表),直接 DETACH 或 DROP 整个分区:
ALTER TABLE orders DETACH PARTITION orders_2024; -- 一秒,把 2024 摘下来
-- detach 后 orders 只剩 3 行(2024 分区还在,只是不挂父表下了)为什么 DETACH/DROP 比 DELETE 快几个量级?因为它是元数据操作——只是把「这张子表还算不算父表的一部分」改一下,或者直接删掉整个文件,不逐行处理数据。而 DELETE FROM orders WHERE placed_at < '2025-01-01' 要逐行标记删除、写 WAL、事后 VACUUM 回收空间。数据量越大,差距越离谱。「按时间滚动保留 N 个月」的场景,分区几乎是唯一优雅的解法。
收益三:每个分区独立管理
分区还能各管各的:独立建索引、独立 VACUUM/ANALYZE、独立表空间(把冷分区放慢盘、热分区放快盘)。一张巨表的 VACUUM 拆成一个个小分区分别做,不再有「锁全表几小时」的噩梦。
版本演进值得知道
声明式分区是逐版本长成熟的,用之前最好知道你的 PG 版本到哪一步:
- PG 10 —— 声明式分区诞生(此前只能用继承 + 触发器手搓)。
- PG 11 —— 支持分区表上的外键、默认分区、
UPDATE跨分区移动行、哈希分区。 - PG 12 —— 分区裁剪、
ATTACH等性能大幅优化,几千分区也能扛。 - PG 13 —— 支持分区表的逻辑复制、B-tree 去重。
分区不是免费的。分区键必须包含在主键 / 唯一约束里(否则数据库没法保证跨分区唯一),这会影响你的主键设计。分区太多(成千上万)也会拖慢计划期。经验法则:先靠索引 + 归档撑着,单表真到亿级、且有清晰的时间 / 租户切分维度时,再上分区——别一开始就过度分区。
横向对比
| 能力 | PostgreSQL | MySQL | Oracle |
|---|---|---|---|
| 声明式分区 | PG 10+ 成熟 | 有(RANGE/LIST/HASH/KEY) | 极强(标杆) |
| 分区裁剪 | 有(计划期 + 执行期) | 有 | 有 |
| 分区外键 | PG 11+ 支持 | ❌ 分区表不支持外键 | 支持 |
| 老分区秒删 | DETACH / DROP | DROP PARTITION | 强 |
| 子分区(多级) | 支持(嵌套 PARTITION BY) | 有限 | 强 |
Oracle 的分区是业界标杆(但要钱)。PostgreSQL 的声明式分区经过 10→13 几个版本已相当好用,且是开源里最完整的之一。MySQL 分区有个硬伤:分区表不支持外键,这在有引用完整性需求时很掣肘。
应用场景
- 时序数据滚动保留:日志 / 指标 / 事件按月 RANGE 分区,每月新建下月分区、DROP 掉 N 个月前的——运维成本极低。
- 多租户隔离:按
tenant_idLIST 分区,大租户单独一个分区,便于单独备份 / 迁移。 - 超大表打散热点:没有天然切分维度时用 HASH 分区,让写入均匀分布到多个物理表。
- 冷热分层:热分区放 SSD 表空间,冷分区放大容量慢盘。
小结与下一章
分区把「一张管不动的大表」拆成「一堆好管理的小表」:查询靠裁剪只碰相关分区,老数据靠 DETACH/DROP 秒级下线,维护也拆成小块。它是应对亿级大表最正统的手段——但记得别过早、别过度。
分区解决的是「大表本身」的问题;下一章的物化视图解决另一类问题:那些算一次很贵、又被反复查的聚合结果,能不能预先算好存下来? 👉 物化视图:把慢查询的结果冻起来。
参考文献
- [1] PostgreSQL 官方文档.「5.12. Table Partitioning.」https://www.postgresql.org/docs/current/ddl-partitioning.html —— 声明式分区、RANGE/LIST/HASH、裁剪、维护。
- [2] PostgreSQL 官方文档.「5.12.4. Partition Pruning.」https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITION-PRUNING —— 计划期与执行期裁剪。