开场白:为什么要写这个?
上个月我们线上一个核心业务表炸了。单表 2.3TB,查询 P99 从 30ms 飙到 4.7 秒,vacuum 跑了 12 小时还没跑完。DBA 群里有人开玩笑说 “你们这表已经快成冷存储了”。
我翻遍了官方文档和社区帖子,发现一个尴尬的事实:2026 年了,关于 PostgreSQL 分区的最佳实践还是七零八落。你说官方文档写得全吗?全。但全是语法,没有一个告诉你 “到底该用 Range 还是 Hash”、“分区键选错了怎么救”。
所以这篇东西来了。我把我过去三个月在生产环境搞分区表的所有经验——包括翻车经历——全写出来。
第一步:你到底需不需要分区?
别为了分区而分区。这是最大的坑。
Reddit 上 r/devops 有个哥们说得挺实在:“I’ve seen people partition 50GB tables thinking it’s a magic performance pill. It’s not.”
我的判断标准很简单:
- 单表超过 500GB,或者行数超过 1 亿
- 你的查询有明显的 “时间范围” 或 “地域范围” 特征
- 你需要定期清理旧数据(比如保留最近 90 天)
- 你的 vacuum 跑得越来越慢,开始影响业务
满足两条以上,才值得做分区。
第二步:选分区策略——Range 还是 Hash?
这里我直接给结论,不废话。
| 策略 | 适用场景 | 优缺点 | 我踩过的坑 |
|---|---|---|---|
| Range(范围分区) | 时间序列数据、日志、订单 | 查询局部性好,清理旧数据方便;但热点集中在最新分区 | 分区键选了 created_at,但查询经常用 user_id 过滤,导致全分区扫描 |
| List(列表分区) | 地域、状态枚举值 | 数据分布可控;但扩展性差 | 分区数量固定,新增值需要重建表 |
| Hash(哈希分区) | 均匀分布数据、避免热点 | 数据分布均匀,写入无瓶颈;但范围查询性能差 | 做 JOIN 时分区裁剪失效,被坑了一周 |
我的建议: 80% 的场景用 Range 分区,时间戳做分区键。剩下 20% 如果是均匀写入的场景,用 Hash。
第三步:2026 年最新分区语法实战
PostgreSQL 16+ 的分区语法已经非常成熟了。别再用老式的表继承方案了(那个方法在 Reddit 上被骂成狗了)。
创建分区表
-- 先建主表
CREATE TABLE orders (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
user_id BIGINT NOT NULL,
amount NUMERIC(10,2),
status VARCHAR(20)
) PARTITION BY RANGE (created_at);
-- 创建分区
CREATE TABLE orders_2026_q1 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE orders_2026_q2 PARTITION OF orders
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
-- 别忘了索引
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_status ON orders (status);
重点来了: 索引是在主表上创建的,会自动应用到所有分区。但如果你有分区特有的索引需求,得单独加。
自动创建分区——pg_partman
手动建分区太傻了。2026 年还在手动建分区的人,要么是新手,要么是受虐狂。
-- 安装扩展
CREATE EXTENSION pg_partman;
-- 配置自动分区
SELECT partman.create_parent(
p_parent_table := 'public.orders',
p_control := 'created_at',
p_type := 'native',
p_interval := '1 month',
p_premake := 3
);
-- 设置自动维护
UPDATE partman.part_config
SET infinite_time_partitions = true,
retention = '3 months',
retention_keep_table = false
WHERE parent_table = 'public.orders';
这玩意儿在 Reddit 上被吹爆了。我用了半年,唯一的问题就是第一次配置的时候没理解 p_premake 参数,导致分区没提前创建,凌晨三点报警说写入失败。
第四步:分区裁剪——性能的关键
分区表不裁剪,等于没分区。
-- 好的查询:能裁剪
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE created_at >= '2026-06-01' AND created_at < '2026-07-01';
-- 坏的查询:不能裁剪
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 12345;
第一个查询只会扫描 orders_2026_q2 一个分区。第二个会扫描所有分区。
怎么优化? 如果你经常按 user_id 查,可以考虑在 user_id 上再加一层子分区。
CREATE TABLE orders_2026_q2 PARTITION OF orders
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01')
PARTITION BY HASH (user_id);
CREATE TABLE orders_2026_q2_h0 PARTITION OF orders_2026_q2
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
-- 再建 h1, h2, h3
这样按 user_id 查的时候也能裁剪到具体的哈希分区。
第五步:运维和监控
查看分区大小
SELECT
parent.relname AS parent_table,
child.relname AS partition_name,
pg_size_pretty(pg_total_relation_size(child.oid)) AS size
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'orders'
ORDER BY child.relname;
分区维护自动化
我们团队写了个简单的 cron job:
#!/bin/bash
# 每天凌晨 2 点执行
psql -d mydb -c "CALL partman.partition_data_proc('public.orders', p_interval := '1 month', p_source_table := 'public.orders_old');"
psql -d mydb -c "DELETE FROM public.orders WHERE created_at < NOW() - INTERVAL '90 days';"
常见问题 FAQ
Q: 分区表能改分区键吗?
不能。分区键一旦创建就不能修改。要改只能重建整个表。所以选分区键的时候想清楚。
Q: 新增分区会影响业务吗?
不会。新增分区是 DDL 操作,但 PostgreSQL 16+ 支持 CREATE TABLE 时加 IF NOT EXISTS,不会锁表。
Q: 分区表支持外键吗?
支持,但有限制。外键必须包含分区键,否则不行。这算是个挺恶心的限制。
Q: 数据怎么迁移到分区表?
用 pg_dump 导出,建好分区表后再导入。或者用 INSERT INTO ... SELECT 分批迁移。别想着原地转换——官方不支持。
总结
分区不是银弹。但如果你用对了,性能提升 10-20 倍不是梦。关键就三点:
- 选对分区策略(大部分场景用 Range)
- 确保查询能裁剪分区
- 自动化运维(pg_partman + cron)
最后说一句:别等到表炸了才想起分区。我们就是血的教训。
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "分区表能改分区键吗?",
"acceptedAnswer": {
"@type": "Answer",
"text": "不能。分区键一旦创建就不能修改。要改只能重建整个表。所以选分区键的时候想清楚。"
}
},
{
"@type": "Question",
"name": "新增分区会影响业务吗?",
"acceptedAnswer": {
"@type": "Answer",
"text": "不会。新增分区是 DDL 操作,但 PostgreSQL 16+ 支持 CREATE TABLE 时加 IF NOT EXISTS,不会锁表。"
}
},
{
"@type": "Question",
"name": "分区表支持外键吗?",
"acceptedAnswer": {
"@type": "Answer",
"text": "支持,但有限制。外键必须包含分区键,否则不行。"
}
},
{
"@type": "Question",
"name": "数据怎么迁移到分区表?",
"acceptedAnswer": {
"@type": "Answer",
"text": "用 pg_dump 导出,建好分区表后再导入。或者用 INSERT INTO ... SELECT 分批迁移。"
}
}
]
}
</script>
社区灵感与参考 (References & Community Insights)
本文探讨的架构演进与技术实现方案,深度提炼自 Hacker News、Reddit 等极客社区的真实工程师讨论、线上事故复盘(Post-mortems)以及一线技术博客的实战经验分享。