PostgreSQL的查询计划质量和写入性能,很大程度上取决于两个常被忽视的配置:统计信息的采集精度和页面的填充因子fillfactor。前者决定了优化器对行数的预估是否准确,直接影响是否走索引;后者决定了每个数据页预留多少空间,进而影响更新操作能否触发HOT优化。理解这两个机制的底层原理并合理调整,往往能在不更换硬件的情况下显著改善数据库表现。

一、行数预估不准的根源:统计信息采集机制
PostgreSQL优化器在生成执行计划时,依赖的是ANALYZE命令采集到pg_statistic系统表中的统计信息,包括每列的非空比例、唯一值个数、高频值列表以及直方图边界。预估行数的核心公式可以简化为:预估行数等于表总行数乘以选择率。问题在于,选择率是从直方图和MCV估算出来的,一旦数据分布与统计信息不一致,预估就会严重偏离。
默认情况下,default_statistics_target的值是100,意味着每列存储100×100=10000个采样数据来构建直方图。对于数亿行的大表,如果某些列存在严重的数据倾斜,比如某个状态字段90%的值都是同一个,直方图无法捕捉这种极端分布,就可能出现预估行数和实际行数相差几个数量级的情况。可以通过EXPLAIN ANALYZE对比rows列的预估值和actual time部分的实际行数来确认偏差。
提高统计精度的方法很简单,针对特定列单独调大统计目标:
-- 针对状态列提高统计精度 ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000; -- 重新采集统计信息 ANALYZE orders;
调整后再次执行EXPLAIN,通常能看到预估值明显接近真实值。代价是ANALYZE耗时增加和pg_statistic存储变大,但在大表场景这点开销完全值得。
二、多列相关性与扩展统计信息
优化器有一个先天假设:不同列之间相互独立。因此当查询条件涉及多个列时,优化器会把各列的选择率直接相乘。如果列之间存在相关性,比如城市列和邮编列高度关联,这种假设就会导致预估行数被严重低估,进而让优化器错误地选择嵌套循环连接或者错误的连接顺序。
比如查询某城市某邮编的订单,城市选择率是百分之一,邮编选择率也是百分之一,优化器按独立假设计算出的组合选择率是万分之一,而实际匹配行数可能远高于此。执行计划因此可能放弃本该使用的索引。
PostgreSQL从10版本开始提供扩展统计信息来解决这个问题:
-- 创建函数依赖统计,让优化器感知列相关性 CREATE STATISTICS stat_city_zip (dependencies) ON city, zip_code FROM orders; ANALYZE orders; -- 查看统计结果 SELECT * FROM pg_stats_ext WHERE statistics_name = 'stat_city_zip';
除了dependencies类型的函数依赖统计,还支持ndistinct类型的多列唯一值统计和mcv类型的多列高频值统计。对于复杂查询场景,可以组合使用这些统计类型,让优化器获得更接近现实的估算依据。需要注意的是,扩展统计只对等值条件有效,范围条件目前仍然无法利用。
三、fillfactor的作用原理与HOT更新
fillfactor控制插入数据时页面的填充程度,取值范围10到100。默认值100意味着页面被尽可能填满后再开辟新页。这个设置对纯插入的表没有问题,但对频繁更新的表是灾难。因为PostgreSQL的多版本并发控制机制下,每次更新都会产生一个新版本的行,如果原页面已经没有空间容纳新行,新行就必须写入其他页面,导致旧页面出现空洞,索引也需要新增指针。
HOT(Heap-Only Tuple)更新是PostgreSQL的重要优化:当更新产生的新行能够放入原页面,且被更新的列上没有索引时,所有索引都不需要修改,新行通过页面内的行指针链与旧行关联。这大幅减少了索引写入量和WAL日志量。而要让HOT更新成为可能,页面里必须有剩余空间,这正是fillfactor的价值所在。
查看当前表的HOT更新比例:
SELECT relname, n_tup_upd, n_tup_hot_upd,
round(n_tup_hot_upd::numeric / nullif(n_tup_upd, 0) * 100, 2) as hot_ratio
FROM pg_stat_user_tables
WHERE relname = 'orders';如果hot_ratio长期低于50%,说明大部分更新没有走HOT路径,此时就该考虑调整fillfactor了。
四、fillfactor的调整方法与场景建议
修改表的fillfactor语法如下,注意这个操作只影响后续的新数据页,已有页面不会重组,需要配合VACUUM FULL或pg_repack才能对存量数据生效:
-- 设置填充因子为80,预留20%空间 ALTER TABLE orders SET (fillfactor = 80); -- 对存量数据生效,VACUUM FULL会锁表 VACUUM FULL orders; -- 生产环境推荐使用pg_repack在线重建 -- pg_repack -t orders -d mydb -U postgres
取值需要结合业务特征权衡。更新频繁、行大小基本不变的OLTP表,建议设置为70到90;有大量插入又有随机更新的表,80左右是常见起点;几乎只插入不更新的日志表、审计表,保持默认100即可,预留空间反而浪费存储并拉长全表扫描时间。索引也有自己的fillfactor,B-tree索引默认90,对于频繁随机插入的索引可以适当调低以减少页面分裂。
调整后需要持续观察验证,除了前面提到的HOT比例,还应关注表和索引的膨胀情况。可以定期使用pgstattuple扩展检查实际空闲空间占比,或者观察pg_stat_user_tables中n_dead_tup的增长速度是否放缓。通常fillfactor从100降到80后,更新密集型表的HOT比例能提升到80%以上,表膨胀速度明显下降,autovacuum的压力也随之减轻。
总的来说,统计信息调优解决的是读路径的计划质量,fillfactor调优解决的是写路径的存储效率,两者配合使用才能让大表在长期运行中保持稳定性能。建议在业务低峰期操作,并做好调整前后的指标对比,用数据验证每一项变更的实际收益。
PostgreSQLfillfactor统计信息修改时间:2026-09-04 10:19:52