运维笔记

PostgreSQL 分区表最佳实践:从踩坑到真香,一个老司机的血泪总结

Developer Tools 技术可视化

兄弟们,今天聊点硬核的——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 - 1TB8-32 个30-100GB中型业务表
1TB - 10TB32-128 个50-200GB日志、事件
> 10TB128-512 个100-500GB超大规模时序

关键点: 每个分区的大小尽量保持一致。如果有的分区 10GB,有的 500GB,那大分区会成为热点,分区裁剪效率直线下降。

PG17 新特性:SPLIT 和 MERGE,真香!

终于等到这一天。以前想拆分区或者合并分区,那叫一个痛苦——新建表、迁移数据、删旧表、更新元数据,一套操作下来,手都抖了。

PG17 直接给了 SPLIT PARTITIONMERGE 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 个分区,每个分区上都有索引,那 VACUUMANALYZE 的时间会线性增长。我们测试过,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(多了一些元数据开销)。

总结

分区表不是银弹,但用对了是真香。核心就三点:

  1. 分区键选对——跟你的查询模式走
  2. 分区数量适中——别贪多,50-200 个分区是最佳区间
  3. 自动化维护——别手动,用工具

最后送大家一句话:先跑通,再优化,别在分区表上过度设计。 我们见过太多人在只有 100 万行数据的时候就搞了 64 个分区,结果查询比不分区还慢。等你数据到 1 亿行再分区,完全来得及。