当 PostgreSQL 单表行数涨到千万甚至上亿,即便建了索引,写入和范围查询也会明显变慢:索引树越来越高、VACUUM 越来越久、删除历史数据要整表扫描。分区表(Partitioning)把一张大表按时间或键值拆成多个子表,让查询只扫命中的分区、清理历史只需 DROP 一个分区。本文用可运行的 SQL,讲清 PostgreSQL 分区表的策略选型、创建、分区裁剪与自动维护。
一、为什么大表需要分区
随着数据增长,单表会暴露三个典型痛点:B+Tree 索引随行数膨胀,点查也要读更深的层级;VACUUM / ANALYZE 在大表上持有更长锁;想删掉一个月数据用 DELETE 会产生海量死元组并长时间阻塞,而 DROP 一个分区只是瞬时元数据操作。
当你出现以下信号,就该考虑分区:单表超过 5000 万行;业务按时间保留滚动窗口(如只留 12 个月);需要把冷热数据分离到不同存储介质。如果你更关心慢 SQL 本身,可以先看 PostgreSQL 慢查询优化实战,分区与索引调优常常要一起做。
二、三种分区策略怎么选
2.1 RANGE 分区(按时间最常用)
按连续范围切分,最典型是按月或按天。日志、订单、监控指标这类天然带时间戳的数据首选 RANGE,配合滚动保留非常顺手。
2.2 LIST 分区(按枚举值)
按固定枚举切分,比如按地区、租户 ID 或订单状态。适合多租户隔离、按区域分库的场景,但分区数量随枚举增长,需要预先规划。
2.3 HASH 分区(均匀打散)
对没有明显范围特征的键(如 user_id)取模打散,避免单分区写入过热。代价是 HASH 分区无法做范围裁剪,范围查询仍要扫全部分区。
| 策略 | 典型键 | 范围裁剪 | 滚动维护 | 适用场景 |
| RANGE | 时间 | ✅ 支持 | 需工具 | 日志 / 订单 / 指标 |
| LIST | 地区 / 租户 | ⚠️ 仅等值 | 手动 | 多租户 / 分区域 |
| HASH | 哈希键 | ❌ 不支持 | 手动 | 均匀打散写入 |
三、动手创建 RANGE 分区表
PostgreSQL 10+ 的声明式分区(declarative partitioning)最简洁。父表只定义结构、本身不存数据,子分区用 PARTITION OF 挂载。
-- 父表(声明式分区,本身不存数据)
CREATE TABLE orders (
id bigserial,
user_id bigint NOT NULL,
amount numeric(12,2),
created_at timestamptz NOT NULL DEFAULT now()
) PARTITION BY RANGE (created_at);
-- 分区键必须出现在主键 / 唯一约束中
ALTER TABLE orders ADD PRIMARY KEY (id, created_at);
-- 2026 年 10 月分区
CREATE TABLE orders_2026_10 PARTITION OF orders
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
-- 2026 年 11 月分区
CREATE TABLE orders_2026_11 PARTITION OF orders
FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');
插入时无需指定分区,优化器按 created_at 自动路由到对应子表:
-- 插入时无需指定分区,优化器按 created_at 自动路由
INSERT INTO orders (user_id, amount, created_at)
VALUES (1024, 99.00, '2026-10-15 09:30:00');
-- 若插入的值落在所有分区之外会报错;可用 DEFAULT 分区兜底
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
⚠️ DEFAULT 分区会”截留”本该进未来新分区的数据。新建分区前必须先 DETACH 它,否则新建分区会失败:
-- 建新分区前,先 DETACH DEFAULT 分区
ALTER TABLE orders DETACH PARTITION orders_default;
CREATE TABLE orders_2026_12 PARTITION OF orders
FOR VALUES FROM ('2026-12-01') TO ('2027-01-01');
ALTER TABLE orders ATTACH PARTITION orders_default
FOR VALUES DEFAULT;
四、分区裁剪让查询只扫需要的分区
分区裁剪(partition pruning)是性能核心:查询只访问 WHERE 命中的分区。默认已开启,用 EXPLAIN 验证最直观:
-- 开启分区裁剪(默认开),用 EXPLAIN 验证
SET enable_partition_pruning = on;
EXPLAIN (COSTS OFF)
SELECT * FROM orders
WHERE created_at >= '2026-10-01' AND created_at < '2026-11-01';
-- 计划里只出现 orders_2026_10,其它分区被剪掉
两个关键注意点:裁剪依赖”分区键出现在 WHERE 里”且是范围/等值表达式,避免对分区键套函数(如 date(created_at)),否则优化器无法裁剪;每个分区都要有自己的本地索引,PostgreSQL 没有全局索引,唯一约束也只能在本分区内保证。
| 项 | 说明 |
| 本地索引 | 每个分区各自建索引,最常用 |
| 全局索引 | PG 原生不支持,需用扩展或触发器模拟 |
| 唯一约束 | 必须包含分区键,否则无法保证跨分区唯一 |
五、自动维护:用 pg_partman 管理滚动窗口
手动建/删分区很快会漏,生产环境用 pg_partman 自动滚动最稳。它负责预建未来分区、按保留策略清理旧分区,把运维从人工脚本里解放出来。
-- 安装扩展
CREATE EXTENSION IF NOT EXISTS pg_partman;
-- 交给 partman 管理:按月、保留 12 个分区、预建 4 个未来分区
SELECT partman.create_parent(
p_parent_table => 'public.orders',
p_control => 'created_at',
p_type => 'native',
p_interval => '1 month',
p_premake => 4
);
-- 配置保留策略(只留最近 12 个月)
UPDATE partman.part_config
SET retention = '12 months',
retention_keep_table = false -- 直接 DROP,而非只 detach
WHERE parent_table = 'public.orders';
用 pg_cron 每天跑一次维护,既建未来分区又清旧分区:
-- 用 pg_cron 每天凌晨跑维护(建未来分区 + 清旧分区)
SELECT cron.schedule(
'partman_orders',
'0 2 * * *',
$$SELECT partman.run_maintenance('public.orders');$$
);
预建未来分区能避免”写入落在空档”报错;retention 用 DROP 比 DELETE 省事且瞬时完成。也可以用 GitHub Actions 在发布流程里跑迁移,把分区表的建表脚本纳入版本管理。
六、常见误区与排障
| 误区 | 现象 | 解决 |
| 对分区键套函数 | 不裁剪、全表扫 | WHERE 直接用原列 |
| DEFAULT 分区塞满 | 新分区建不了 | 先 DETACH 再建 |
| 唯一约束不含分区键 | 建表失败 | 主键加 created_at |
| 忘了给分区建索引 | 分区内慢查 | 建父表时建(自动继承) |
| 期望跨分区唯一 | 无法保证 | 唯一键含分区键或业务层控制 |
七、生产落地检查清单
| 检查项 | 建议 |
| 分区键 | 选高频过滤列(时间 / 租户) |
| 主键 | 必须含分区键 |
| 裁剪验证 | EXPLAIN 确认只扫命中分区 |
| 索引 | 每个分区建本地索引 |
| 维护 | pg_partman + pg_cron 自动滚动 |
| 保留 | retention DROP 旧分区 |
| 监控 | 关注分区数量与未来分区是否预建 |
八、和 TimescaleDB、物化视图怎么选
三者并不冲突,常组合使用:如果你的数据本质是时序指标,TimescaleDB 实战 的 hypertable 在压缩与连续聚合上更省心;如果是为了加速固定聚合报表,PostgreSQL 物化视图实战 可能比分区更直接;而分区表解决的是”大表的写入、清理与裁剪”。需要跨库同步分区数据时,可参考 PostgreSQL 逻辑复制实战。高可用部署可结合 PostgreSQL 高可用架构 一起规划。
结语
分区表不是银弹,但对”单表上亿 + 滚动保留”的场景几乎是必选项。先用 RANGE 按时间切,配 pg_partman 自动滚动,再用 EXPLAIN 验证裁剪,就能把大表查询稳定压在毫秒级。配合 PostgreSQL 慢查询优化 一起看,性能收益会更明显。在容器里跑 PostgreSQL 可参考 Docker 实战 的部署要点。




