分区表是否值得建取决于写入热点、查询模式和运维成本三方面平衡,而非单纯看表大小;需结合pg_total_relation_size、查询频次及剪枝效果综合判断。
分区表到底值不值得建?先看 pg_total_relation_size 和查询频次
一张表要不要分区,不是看它“大不大”,而是看它“胖不胖”+“动不动”。pg_total_relation_size 返回的字节数只是表体积的静态快照,真正关键的是:这体积里有多少是历史冷数据、有多少是每天被 insert/update 频繁修改的热区、以及查询是否总带时间/状态等高选择性过滤条件。
实操建议:
- 如果
pg_total_relation_size('orders')超过 50GB,但 90% 查询只查最近 7 天数据,且WHERE created_at >= '2024-06-01'能命中索引,那分区大概率划算 - 如果表才 8GB,但每天有 200+ 并发
UPDATE修改随机行,且查询几乎不带分区键过滤(比如只按user_id查),加分区反而拖慢写入、增加 planner 开销 - 用
EXPLAIN (ANALYZE, BUFFERS)对比分区前后的执行计划,重点看Seq Scan是否减少、Buffers是否显著下降——别只信“扫描行数”
并发 DML 下,分区键选错等于自废索引
PostgreSQL 分区表本身不自动优化写入,并发 INSERT 或 UPDATE 的性能瓶颈往往卡在分区路由和锁竞争上。选错分区键会让所有写操作挤在同一个子表里,甚至引发 tuple concurrently updated 错误。
常见错误现象:
- 按
created_at按月分区,但业务写入集中在当月最后一天,导致当月分区成为热点 - 用
user_id % 100做哈希分区,但 20% 的用户贡献了 80% 的订单,部分子表膨胀严重、其余空闲 -
UPDATE语句更新分区键字段(如SET created_at = now()),触发跨分区移动,锁住两个子表 + 触发全量行复制
实操建议:
- 写多读少场景,优先选能天然打散写入的字段,比如
order_id(UUID 或递增序列)、tenant_id,而非时间戳 - 必须用时间分区时,把粒度调细一点(比如按天而非按月),并配合
pg_partman自动预建未来 3–7 天的分区,避免 INSERT 时动态创建锁表 - 禁止在
UPDATE中修改分区键;若业务真需要改,先DELETE再INSERT到新分区,显式控制生命周期
查询维度和分区剪枝失效的三种典型情况
分区剪枝(partition pruning)不是魔法,它依赖查询条件能被 planner 静态推导出可排除的分区。很多看似合理的 WHERE 条件,实际根本剪不掉。
使用场景与参数差异:
- 用
WHERE created_at BETWEEN $1 AND $2通常能剪枝;但换成WHERE created_at >= current_date - INTERVAL '7 days',某些 PG 版本(如 12 之前)会因无法确定运行时值而放弃剪枝 - JOIN 中用分区表做驱动表,被驱动表字段参与分区键计算(如
ON o.created_at = d.report_date),planner 无法提前判断 d.report_date 范围,剪枝失效 - 查询中混用
OR条件(如WHERE status = 'paid' OR created_at > '2024-01-01'),即使后者能剪枝,整个条件也可能让 planner 退化为全分区扫描
实操建议:
- 在生产 SQL 中避免对分区键使用函数或表达式,宁可用
WHERE created_at >= '2024-06-01'::date而非WHERE date(created_at) >= '2024-06-01' - 用
EXPLAIN看执行计划里是否出现Partition Filter行,没有就说明剪枝失败——别靠感觉 - 对复杂查询,考虑用
UNION ALL显式指定要查的几个分区,比依赖 planner 更可控(尤其在报表类场景)
Segment 大小不是越小越好,100MB 是个经验拐点
很多人一上来就想把分区切得极细(比如每小时一个分区),结果发现 vacuum 变慢、catalog 膨胀、planner 解析计划变卡。PostgreSQL 对子表数量敏感,不是越多越好。
性能与兼容性影响:
- 单个分区物理大小低于 100MB 时,I/O 效率开始下降,因为磁盘顺序读优势减弱,随机访问占比上升
- 超过 500 个子表后,
VACUUM单次耗时明显增长(每个子表都要单独扫描 FSM、更新 relfrozenxid),可能拖垮维护窗口 - 某些旧版客户端或 ORM(如老版本 Django + psycopg2)在处理大量子表时,元数据查询(
pg_inherits)响应变慢,甚至超时
实操建议:
- 目标单分区大小控制在 100MB–2GB 之间,具体看数据写入速率:日增 500MB 就按天分区,日增 5GB 就按半天或按周
- 用
SELECT pg_size_pretty(pg_total_relation_size(oid)) FROM pg_class WHERE relkind = 'r' AND relname ~ '^orders_p[0-9]{8}$'快速检查各分区大小分布 - 上线前跑一次
EXPLAIN+VACUUM ANALYZE,确认分区数量增长到预期上限时,planner 仍能在 100ms 内生成计划
分区设计最麻烦的地方不在语法,而在你得同时盯着写入热点、查询模式、运维成本三根线——哪根松了,另外两根就容易崩。尤其是 pg_partman 自动建分区时,默认参数常忽略你的实际 DML 压力,得手动调。










