别再盲设 70:用真实行大小计算 PostgreSQL 表的 Fillfactor

2026-08-27 33 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:8 分钟

PostgreSQL 的 fillfactor 是一个很小、却经常被忽略的设置。它决定数据首次写入时,每个表页要填多满,并为后续更新预留多少空间。对频繁更新的表来说,合适的预留空间可能提高 HOT update 的机会,减少索引改写、WAL 和 I/O;但把所有表统一设成 70 或 80,同样是一种猜测。

更稳妥的做法是:先用真实数据估算平均行大小,再结合更新频率和监控结果调整。

Fillfactor 到底控制什么

PostgreSQL 的表页通常是 8 KB,也就是 8,192 字节。fillfactor 越低,初次写入时页面留下的空闲空间越多。

这块空间的价值在于:当一行被更新、且新版本仍能放进原页面时,PostgreSQL 有机会把新版本放在旧版本附近,形成 HOT(Heap-Only Tuple)更新。HOT 更新可以避免为这次更新修改表上的每个索引,因此通常比需要移动到其他页面并同步维护索引的更新更便宜。

但它不是免费的优化:

  • Fillfactor 越低,表需要的页面越多,读取和存储空间可能增加。
  • 预留空间只解决“页面里是否有位置”的一部分问题,更新后的行如果明显变大,空间仍可能不够。
  • 如果更新的列参与索引,或者表本身存在严重膨胀,单独调整 fillfactor 不会自动解决所有问题。

因此,fillfactor 应该按表的行大小和更新模式分别决定。

一个可操作的起始公式

可以先使用下面的估算:

平均行占用百分比 = 平均行大小 ÷ 8,192 × 100
起始 fillfactor = 100 − 平均行占用百分比

例如,某张表的平均行大小约为 2,300 字节:

(2,300 ÷ 8,192) × 100 ≈ 28%
起始 fillfactor = 100 − 28 = 72

72 可以作为一个起点。它大致为每个页面保留一行平均大小的空间。

如果这张表全天都在被更新,一次更新还不够,页面可能很快再次被填满。此时可以继续降低,例如从 72 试到 6050,给同一页面上的多次重写留下更多余量。具体数值应通过监控验证,而不是直接套用。

用 PostgreSQL 统计真实数据

下面的查询可以在最近执行过 ANALYZE 后运行。把表名替换成实际需要检查的表:

SELECT
    relname AS table_name,
    n_live_tup,
    pg_relation_size(relid) AS total_bytes,
    pg_relation_size(relid) / NULLIF(n_live_tup, 0)
        AS avg_row_size_bytes,
    pg_size_pretty(
        pg_relation_size(relid) / NULLIF(n_live_tup, 0)
    ) AS avg_row_size_pretty
FROM pg_stat_user_tables
WHERE relname IN ('orders', 'order_events', 'user_sessions')
ORDER BY relname;

拿到 avg_row_size_bytes 后,代入公式即可得到一个初始值。也可以直接让 SQL 计算建议值:

SELECT
    relname AS table_name,
    n_live_tup,
    ROUND(
        100 - (
            pg_relation_size(relid)::numeric
            / NULLIF(n_live_tup, 0)
            / 8192
            * 100
        )
    ) AS suggested_fillfactor
FROM pg_stat_user_tables
WHERE relname IN ('orders', 'order_events', 'user_sessions')
ORDER BY relname;

这只是估算,不是精确的元组大小测量。n_live_tup 是统计估计值,而 pg_relation_size() 还会受到死元组和表膨胀影响。如果表很久没有维护或刚经历大量更新,计算出来的“平均行大小”可能被膨胀放大。

建议先执行:

ANALYZE orders;
ANALYZE order_events;
ANALYZE user_sessions;

然后再运行统计查询。对于膨胀明显的表,还要把查询结果与实际表状态、维护历史一起判断。

从估算走向验证

查看表的 HOT 更新比例,可以帮助判断当前设置是否有效:

SELECT
    relname AS table_name,
    n_tup_upd AS total_updates,
    n_tup_hot_upd AS hot_updates,
    ROUND(
        100.0 * n_tup_hot_upd / NULLIF(n_tup_upd, 0),
        2
    ) AS hot_update_pct,
    n_live_tup,
    n_dead_tup
FROM pg_stat_user_tables
WHERE relname IN ('orders', 'order_events', 'user_sessions')
ORDER BY hot_update_pct NULLS FIRST;

实践中可以按下面的顺序调整:

  1. 对目标表执行 ANALYZE,记录平均行大小和当前 HOT 比例。
  2. 根据公式得到初始值,例如 72
  3. 对高频更新表选择更低的试验值,例如 6050
  4. 观察几天到数周的 HOT 更新比例、WAL 生成量、I/O、死元组和表膨胀。
  5. 如果空间成本上升却没有带来更好的更新行为,再逐步提高 fillfactor。

设置可以这样改造。请先在测试环境或低风险维护窗口验证,因为改变表的存储参数通常需要配合后续的重写或维护,已有页面不会因为这一条命令就自动按新比例重新排布:

ALTER TABLE orders SET (fillfactor = 60);
ALTER TABLE order_events SET (fillfactor = 50);

不要把这条命令批量应用到所有表。只更新不修改的日志表、归档表或大多数只读表,往往没有理由为了 HOT 更新牺牲额外空间;而频繁更新、行大小相对稳定的业务表,才是优先候选。

一份实际落地清单

  • 先确认表是否真的存在高频更新,而不是只看查询慢。
  • 在最近一次 ANALYZE 后获取统计数据。
  • 用真实平均行大小计算起始值,不要默认 70 或 80。
  • 对更新后的行可能显著变大的表保持谨慎。
  • 同时观察 HOT 比例、WAL、I/O、死元组和膨胀。
  • 通过小步调整验证收益,并为表空间增长留出预算。

Fillfactor 不是一个“设置一次就结束”的参数。它更像是页面空间、更新成本和存储容量之间的调节旋钮:先用数据得到合理起点,再用生产指标决定是否继续降低或恢复。对于真正被更新流量压着跑的表,这个小设置才有机会带来可观的差异。


相关推荐