兄弟们,今天聊点硬核的——PostgreSQL 分区表。
先别急着划走。我知道你们在 Reddit 上看到过一堆吐槽,什么“分区表就是个坑”、“查询计划慢成狗”、“维护起来想骂娘”。没错,这些我都经历过。上个月我们生产环境的一个分区表差点把监控搞炸了,原因是分区数量冲到 5000+,规划器直接摆烂。
但话说回来,分区表这东西,用好了是真香。P99 从 2.1s 砍到 380ms 的案例我手里就有好几个。关键是——你得知道怎么用。
分区键选不对,后面全白费
这是最常翻车的地方。很多人上来就问:“我该用范围分区还是哈希分区?”
我的答案是:先看你的查询模式。
范围分区(RANGE)
适合时间序列数据。日志、订单、事件——按天或按月切,查询带时间范围过滤,分区裁剪直接起飞。
举个栗子:
CREATE TABLE orders (
order_id bigserial,
created_at timestamptz NOT NULL,
user_id bigint,
amount numeric
) PARTITION BY RANGE (created_at);
-- 按月分区
CREATE TABLE orders_2026_06 PARTITION OF orders
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
注意事项: 分区边界必须严格连续,不能有重叠。而且,如果你查询 WHERE created_at > now() - interval '7 days',但分区是按月的,那分区裁剪只能排除掉整月,粒度不够细。
哈希分区(HASH)
适合数据分布均匀但没自然时间维度的场景,比如用户表、设备表。
CREATE TABLE users (
user_id bigint,
name text,
email text
) PARTITION BY HASH (user_id);
-- 创建 4 个分区
CREATE TABLE users_0 PARTITION OF users
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE users_1 PARTITION OF users
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
踩坑实录: 哈希分区数量最好是 2 的幂次(4、8、16、32)。为什么?因为以后要 SPLIT PARTITION 或者 MERGE PARTITIONS(PG17 新特性),2 的幂次能让你重算 REMAINDER 的时候少掉头发。
列表分区(LIST)
这个用得少,但特定场景很香。比如按地区、按状态。
CREATE TABLE orders_archive PARTITION OF orders
FOR VALUES IN ('closed', 'cancelled');
分区数量的黄金法则
Reddit 上有个老哥吐槽:“我分了 10000 个分区,查询慢到爆炸。” 兄弟,你这是自找的。
分区不是越多越好。 官方文档说得很清楚:分区约束里应该只包含分区列和常量的比较,用 B-tree 索引能支持的那种。
我个人的经验法则:
| 数据量 | 建议分区数 | 分区大小 | 典型场景 |
|---|---|---|---|
| < 100GB | 不分区或 4-8 个 | 10-50GB | 小表、配置表 |
| 100GB - 1TB | 8-32 个 | 30-100GB | 中型业务表 |
| 1TB - 10TB | 32-128 个 | 50-200GB | 日志、事件 |
| > 10TB | 128-512 个 | 100-500GB | 超大规模时序 |
关键点: 每个分区的大小尽量保持一致。如果有的分区 10GB,有的 500GB,那大分区会成为热点,分区裁剪效率直线下降。
PG17 新特性:SPLIT 和 MERGE,真香!
终于等到这一天。以前想拆分区或者合并分区,那叫一个痛苦——新建表、迁移数据、删旧表、更新元数据,一套操作下来,手都抖了。
PG17 直接给了 SPLIT PARTITION 和 MERGE PARTITIONS:
-- 把一个大分区拆成两个
ALTER TABLE orders SPLIT PARTITION orders_2026_06 INTO
(PARTITION orders_2026_06_early VALUES FROM ('2026-06-01') TO ('2026-06-15'),
PARTITION orders_2026_06_late VALUES FROM ('2026-06-15') TO ('2026-07-01'));
-- 合并两个小分区
ALTER TABLE orders MERGE PARTITIONS (orders_2026_01, orders_2026_02)
INTO PARTITION orders_2026_q1;
但注意: 这个操作会锁表。虽然比手动迁移快得多,但生产环境还是建议在低峰期执行。
索引策略:别傻傻地在父表上建索引
这个坑我踩过。在父表上建索引,子分区也会自动继承?不会。
正确的做法是:在每个子分区上单独建索引。或者用模板:
-- 在父表上创建模板索引,新分区自动继承
CREATE INDEX ON orders (created_at, user_id);
这样新创建的分区会自动带上这个索引。
但这里有个隐藏问题: 如果你有 128 个分区,每个分区上都有索引,那 VACUUM 和 ANALYZE 的时间会线性增长。我们测试过,128 个分区的 ANALYZE 时间比单表多了 3.5 倍。
分区裁剪(Partition Pruning)—— 救星还是坑?
分区裁剪是分区表性能的核心。如果查询不能有效裁剪分区,那分区表比普通表还慢。
什么情况下裁剪失败?
- 查询条件里用了函数包装分区列:
WHERE date_trunc('month', created_at) = '2026-06-01'—— 这会导致全表扫描所有分区。 - 动态参数类型不匹配:
WHERE created_at = '2026-06-15'如果 created_at 是 timestamptz,字符串会被隐式转换,但裁剪可能失效。
正确写法:
-- 好
WHERE created_at >= '2026-06-01' AND created_at < '2026-07-01'
-- 不好
WHERE date_trunc('month', created_at) = '2026-06-01'
维护操作:自动化是唯一的出路
手动维护分区表?你是在开玩笑吗。我们团队用 pg_partman 来自动化分区管理:
-- 安装 pg_partman
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 -- 预创建未来 3 个月的分区
);
这样它会自动创建新分区、清理旧分区。配合 pg_cron 定时执行,基本不用人工干预。
FAQ
分区表有什么缺点?
分区太多会炸规划器。官方建议分区数控制在 1000 以内,超过 5000 基本就废了。另外,跨分区查询(比如 SELECT * FROM orders WHERE user_id = 123 没有时间条件)会扫描所有分区,比普通表慢得多。
PostgreSQL 分区是怎么工作的?
逻辑上是一个父表 + 多个子表。查询发到父表,规划器根据分区键裁剪掉不需要的子表,只扫描匹配的分区。PG17 之前用继承实现,现在推荐用原生分区(PARTITION BY 语法)。
Sharding 和分区哪个好?
两个东西。分区是单库内的水平切分,sharding 是跨库。分区解决的是单表过大导致的性能问题,sharding 解决的是单库容量上限。我一般建议:先分区,不够再 sharding。别一上来就 sharding,那运维复杂度是几何级增长的。
分区表能提高性能吗?
能,但前提是查询能被裁剪。如果查询条件里没有分区键,分区表只会更慢。我们测试过一个 5 亿行的订单表,按时间分区后,带时间范围的查询从 12s 降到 400ms。但全表扫描的查询,从 12s 变成 15s(多了一些元数据开销)。
总结
分区表不是银弹,但用对了是真香。核心就三点:
- 分区键选对——跟你的查询模式走
- 分区数量适中——别贪多,50-200 个分区是最佳区间
- 自动化维护——别手动,用工具
最后送大家一句话:先跑通,再优化,别在分区表上过度设计。 我们见过太多人在只有 100 万行数据的时候就搞了 64 个分区,结果查询比不分区还慢。等你数据到 1 亿行再分区,完全来得及。