用 PostgreSQL HOT Update 调整 Fillfactor:HammerDB TPROC-C 基准测试中的膨胀控制

2026-08-05 60 预计阅读时间: 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.

预计阅读时间:9 分钟

在高频更新的 PostgreSQL 工作负载中,Vacuum 往往被视为必须持续追赶的后台任务。但有一条更直接的优化路径:让更新尽可能使用 HOT(Heap-Only Tuple),把新版本留在同一个数据页中,从而减少索引维护、降低表膨胀增长,并减少 Vacuum 的压力。

Avi Vallarapu 的 HammerDB TPROC-C 基准测试聚焦于一个实用问题:调整表的 fillfactor 后,PostgreSQL 能否为 HOT 更新预留足够空间。测试结果表明,合理设置 fillfactor 可以显著改善高更新表的空间行为;在该基准场景中,部分表甚至不再需要通过 Vacuum 来持续遏制膨胀增长。

HOT Update 解决了什么问题

PostgreSQL 的普通更新通常不会原地修改旧行,而是写入一条新的行版本,并把旧版本标记为不可见。新行版本如果需要跨页写入,更新可能带来更多页面访问和索引维护。

HOT 更新的关键条件是:

  • 新版本能够放在原行所在的数据页中。
  • 更新没有改变被索引的列。
  • 页面中存在足够的空闲空间。

当这些条件满足时,PostgreSQL 可以只在堆表页面中创建新版本,并通过 HOT 链关联旧版本。相关索引不必为每个新版本增加一条索引项,这会降低更新成本,也有助于减缓表和索引的膨胀。

fillfactor 控制新建或重写表时,一个页面预先填充到什么程度。例如,表的 fillfactor 设置为 70,意味着初始装载时大约保留 30% 的页面空间,供后续更新使用。它不是运行时强制保留每页固定比例的空洞,也不会自动回收已经形成的膨胀。

为什么 Fillfactor 会影响 Vacuum

对频繁更新的业务表而言,较高的填充率通常意味着页面更满,更新后的新版本更容易被迫写到其他页面。这样会降低 HOT 更新比例,增加索引项写入,并让死元组和空间碎片更快累积。

降低 fillfactor 会牺牲一部分初始存储密度,换取更新阶段的页面余量。这个取舍是否值得,取决于几个因素:

  • 哪些列会被频繁更新。
  • 更新行的平均大小是否会增长。
  • 页面中是否有足够空间容纳新版本。
  • 更新列是否被索引。
  • 表和索引的读写比例,以及磁盘容量约束。

因此,不应把所有表统一设置成一个低值。订单状态、库存数量、账户余额这类频繁更新的表,通常比只追加数据的日志表更值得单独评估。

还需要注意,HOT 并不能让 Vacuum 永久消失。Vacuum 仍然负责清理不可见版本、维护可见性信息,并防止事务 ID 回卷。更准确的目标是:通过提高 HOT 更新比例,让 Vacuum 不再频繁地被膨胀增长牵着走。

可以这样检查和调整

下面是一组可直接改造的 psql 示例。请先把 orders 替换成真实表名,并在测试环境或低峰窗口执行。ALTER TABLE ... SET (fillfactor = ...) 只会影响后续写入和表重写,不会自动把现有页面重新整理成新布局。

-- 1. 查看表当前的存储参数
SELECT relname, reloptions
FROM pg_class
WHERE oid = 'public.orders'::regclass;

-- 2. 查看 HOT 更新比例和 Vacuum 统计
SELECT
    relname,
    n_tup_upd,
    n_tup_hot_upd,
    round(100.0 * n_tup_hot_upd / NULLIF(n_tup_upd, 0), 2) AS hot_update_pct,
    n_live_tup,
    n_dead_tup,
    last_autovacuum,
    last_vacuum
FROM pg_stat_user_tables
WHERE relname = 'orders';

-- 3. 为后续更新预留更多页面空间
ALTER TABLE public.orders SET (fillfactor = 70);

-- 4. 通过表重写让新的 fillfactor 应用于现有数据
-- 生产环境请评估锁影响、磁盘空间和执行窗口
VACUUM (FULL, ANALYZE) public.orders;

-- 5. 更新业务行后再次观察 HOT 比例和死元组变化
SELECT
    relname,
    n_tup_upd,
    n_tup_hot_upd,
    n_dead_tup,
    last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'orders';

如果不能接受 VACUUM FULL 带来的表级锁,可以把重写方案放到维护窗口,或者使用经过评估的在线重组工具。改变参数本身不会立即带来效果,必须确认现有数据页已经通过重写获得可用空间。

可以按 90、80、70 等档位进行小范围压测,并把以下指标一起记录下来:

  • n_tup_hot_upd / n_tup_upd:HOT 更新比例。
  • n_dead_tup:死元组数量及增长速度。
  • pg_stat_user_tables.last_autovacuum:自动 Vacuum 的触发和完成情况。
  • 表大小与索引大小:可结合 pg_total_relation_size() 观察。
  • TPS、平均延迟和磁盘写入量:避免只优化 Vacuum 而损害业务吞吐。

用 HammerDB 场景验证,而不是凭经验下注

TPROC-C 这类基准测试适合观察高并发事务、频繁更新和索引访问共同作用下的结果。可以准备两组尽量一致的环境:一组使用默认或较高的 fillfactor,另一组只调整目标表的 fillfactor,然后在相同的并发数、数据量、测试时长和 PostgreSQL 配置下运行 HammerDB。

测试重点不是某个固定的 fillfactor 数字,而是曲线变化:当预留空间增加时,HOT 比例是否上升,膨胀增长是否放缓,Vacuum 是否减少,以及 TPS 是否仍然满足目标。如果 fillfactor 过低,读操作可能需要访问更多页面,表的初始体积也会增大;如果过高,HOT 又可能因为缺少页面空间而失效。

还要检查被更新列是否存在索引。假设事务只修改 status,但 status 被单独建立了索引,那么这类更新可能无法成为 HOT。索引是查询性能和 HOT 机会之间的实际权衡,应结合执行计划和业务查询来决定。

落地前的检查清单

  • 只对高频更新、膨胀明显的表做针对性调整。
  • 先用 pg_stat_user_tables 记录当前 HOT 比例和死元组增长。
  • 用 HammerDB 或业务回放测试多个 fillfactor 值,而不是直接套用一个数字。
  • 确认更新列与索引之间的关系。
  • 评估表重写所需的锁、时间和额外磁盘空间。
  • 保留 Vacuum 和自动 Vacuum 监控,不要把 HOT 当成 Vacuum 的替代品。

HOT 更新和 fillfactor 的价值在于把问题前移:与其等表膨胀后反复清理,不如在表设计和基准测试阶段为常见更新模式预留空间。最终配置应由 HOT 比例、膨胀速度、业务延迟和存储成本共同决定。


相关推荐