# 表分区 · 把一张大表拆成很多小表

> 声明式分区(PG 10+)把一张逻辑大表按 RANGE / LIST / HASH 拆成多张物理子表。查询靠分区裁剪只扫相关分区，老数据 DETACH/DROP 一秒下线（比 DELETE 快几个量级），每个分区还能独立索引、独立 VACUUM。

- 作者：David（道雾轩）
- 专栏：PostgreSQL 特性详解（https://daiw.org/manual/postgresql.md）
- 最后更新：2026-07-13
- 原文：https://daiw.org/manual/postgresql/partitioning
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 表分区 · 把一张大表拆成很多小表

索引能让大表查得快，但当单表长到几亿、几十亿行，另一些麻烦会冒出来：VACUUM 越来越慢、删除历史数据要锁很久、单个索引大到装不进内存。这时候的解法不是「查得更巧」，而是把大表**物理上拆小**——这就是分区。

PostgreSQL 10 起支持**声明式分区**：你对着一张「父表」写查询，数据库自动把它路由 / 汇总到底下的多张「子表（分区）」。对应用透明，对数据库高效。

## 三种分区策略

分区键决定「一行该去哪个分区」，有三种策略：

**① RANGE（按区间）** —— 最典型，按时间或数值区间分。日志、订单按月分：

```sql
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 各建一个……
```

插入时数据自动落到对应分区，查父表则透明汇总：

```text
orders_2024: 1 行   orders_2025: 2 行   orders_2026: 1 行
orders(父表查询): 4 行   ← 一条 SELECT * FROM orders 汇总所有分区
```

**② LIST（按枚举）** —— 按离散值分，典型是按地区、按租户：

```sql
CREATE TABLE users_cn PARTITION OF users FOR VALUES IN ('CN');
-- users_cn: ['王']  users_us: ['Bob']  users_other: ['Tanaka']
```

**③ HASH（按散列）** —— 没有天然区间 / 枚举时，按哈希均匀打散，避免热点：

```sql
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 **只扫相关分区**，跳过其余全部。

```sql
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 整个分区**：

```sql
ALTER TABLE orders DETACH PARTITION orders_2024;  -- 一秒,把 2024 摘下来
-- detach 后 orders 只剩 3 行(2024 分区还在,只是不挂父表下了)
```

<Callout type="info">
  为什么 DETACH/DROP 比 DELETE 快几个量级？因为它是**元数据操作**——只是把「这张子表还算不算父表的一部分」改一下，或者直接删掉整个文件，**不逐行处理数据**。而 `DELETE FROM orders WHERE placed_at < '2025-01-01'` 要逐行标记删除、写 WAL、事后 VACUUM 回收空间。数据量越大，差距越离谱。「按时间滚动保留 N 个月」的场景，分区几乎是唯一优雅的解法。
</Callout>

## 收益三：每个分区独立管理

分区还能各管各的：**独立建索引、独立 VACUUM/ANALYZE、独立表空间**（把冷分区放慢盘、热分区放快盘）。一张巨表的 VACUUM 拆成一个个小分区分别做，不再有「锁全表几小时」的噩梦。

## 版本演进值得知道

声明式分区是逐版本长成熟的，用之前最好知道你的 PG 版本到哪一步：

- **PG 10** —— 声明式分区诞生（此前只能用继承 + 触发器手搓）。
- **PG 11** —— 支持分区表上的外键、默认分区、`UPDATE` 跨分区移动行、哈希分区。
- **PG 12** —— 分区裁剪、`ATTACH` 等性能大幅优化，几千分区也能扛。
- **PG 13** —— 支持分区表的逻辑复制、B-tree 去重。

<Callout type="warn">
  分区不是免费的。**分区键必须包含在主键 / 唯一约束里**（否则数据库没法保证跨分区唯一），这会影响你的主键设计。分区太多（成千上万）也会拖慢计划期。经验法则：**先靠索引 + 归档撑着，单表真到亿级、且有清晰的时间 / 租户切分维度时，再上分区**——别一开始就过度分区。
</Callout>

## 横向对比

| 能力 | 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_id` LIST 分区，大租户单独一个分区，便于单独备份 / 迁移。
- **超大表打散热点**：没有天然切分维度时用 HASH 分区，让写入均匀分布到多个物理表。
- **冷热分层**：热分区放 SSD 表空间，冷分区放大容量慢盘。

## 小结与下一章

分区把「一张管不动的大表」拆成「一堆好管理的小表」：查询靠裁剪只碰相关分区，老数据靠 DETACH/DROP 秒级下线，维护也拆成小块。它是应对亿级大表最正统的手段——但记得别过早、别过度。

分区解决的是「大表本身」的问题；下一章的物化视图解决另一类问题：**那些算一次很贵、又被反复查的聚合结果，能不能预先算好存下来？** 👉 [物化视图：把慢查询的结果冻起来](https://daiw.org/manual/postgresql/materialized-views)。

## 参考文献

- [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 —— 计划期与执行期裁剪。
