运维笔记

PostgreSQL 分区表实战:2026 年生产环境分区配置与踩坑全记录

Developer Tools 技术可视化

开场白:为什么要写这个?

上个月我们线上一个核心业务表炸了。单表 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 倍不是梦。关键就三点:

  1. 选对分区策略(大部分场景用 Range)
  2. 确保查询能裁剪分区
  3. 自动化运维(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)以及一线技术博客的实战经验分享。

Elvin Hui

关于作者:Elvin Hui

Elvin 拥有 10+ 年企业级数据中心、云原生架构和网络安全经验。持有 CCNA、AWS 解决方案架构师认证。我致力于将一线的“踩坑”经验沉淀为真实、硬核的技术指南,拒绝空洞理论。