news 2026/10/14 8:50:50

PostgreSQL HOT更新:频繁更新表的索引膨胀根因与fillfactor调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL HOT更新:频繁更新表的索引膨胀根因与fillfactor调优

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

PostgreSQL有个很反直觉的现象:一张表的数据量没怎么涨,但索引文件却从几百MB膨胀到了几个GB。磁盘空间被吃掉了,查询反而越来越慢。

这个问题在频繁更新的表上尤其严重。根因就在HOT更新——PostgreSQL针对MVCC更新开销的核心优化机制。理解HOT,就能理解为什么有些表的索引会“偷偷”膨胀。

先搞懂几个词:

HOT(Heap-Only Tuple):PostgreSQL的更新优化机制。当UPDATE不修改任何被索引引用的列,且新行版本能放在与旧行相同的堆页内时,可以跳过索引更新,只在堆内建立新旧行之间的指针。索引不需要新增条目,旧行版本也可以在日常操作(包括SELECT)中直接清理,不需要等VACUUM。

fillfactor:表的填充因子,控制建表或重建时每个数据页预留多少空闲空间。默认100(不预留)。对频繁更新的表,降低fillfactor可以为新行版本预留空间,提高HOT更新的概率。

n_tup_upd / n_tup_hot_upd:pg_stat_all_tables中的两个统计字段,分别记录总更新次数和其中HOT更新的次数。两者的比值直接反映HOT是否生效。

一、HOT的两个前提条件

HOT更新要生效,必须同时满足两个条件:

条件一:UPDATE不修改任何被索引引用的列。包括普通B-tree索引、表达式索引和部分索引。如果UPDATE修改了任何一个索引列,该索引就必须插入新条目,HOT失效。

条件二:新行版本能放在与旧行相同的堆页内。如果旧行所在的数据页没有足够的空闲空间容纳新行,新行会迁移到其他页,HOT也失效。

两个条件同时满足,HOT才会生效。

二、没有HOT时会发生什么

理解HOT失效的代价,才能理解为什么这个问题值得重视。

没有HOT时,每次UPDATE都需要:

  1. 在堆中插入新行版本

  2. 在所有索引中为每个新行版本插入索引条目

  3. 旧行版本和旧索引条目等到VACUUM时才被清理

如果一张表有5个索引,一次UPDATE就要在5个索引中各插入一条新记录。频繁更新的表,索引文件会以远超数据文件的速率膨胀。

更关键的是,索引膨胀后,查询需要扫描更多的索引页才能定位到目标数据,I/O成本上升,缓存命中率下降,形成恶性循环。

三、三个最常见的HOT失效场景

场景一:在频繁更新的列上建了索引

CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, status VARCHAR(20), updated_at TIMESTAMP ); CREATE INDEX idx_status ON orders(status);

每次UPDATE orders SET status = 'SHIPPED'都会修改idx_status索引列,HOT失效。即使status值从'SHIPPED'改成'SHIPPED'(语义上无变化),索引仍然需要插入新条目。

这是最容易被忽略的陷阱。很多人以为“只改了一个索引列,其他索引不受影响”,但实际上PostgreSQL的判断逻辑是“只要UPDATE语句中SET子句引用了索引列,HOT就失效”,而不是“只有值真正变化时才失效”。

场景二:表达式索引和部分索引

CREATE INDEX idx_lower_email ON users(lower(email));

即UPDATE没有修改email列,但如果UPDATE语句的SET子句涉及了email列(即使值没变),表达式索引的HOT也会失效。

场景三:fillfactor=100(默认)

CREATE TABLE orders (...) WITH (fillfactor = 100);

fillfactor=100意味着每个数据页完全填满,没有为新行版本预留任何空间。即使UPDATE不修改索引列,新行也找不到位置,只能迁移到新页,HOT失效。

fillfactor 90意味着每页预留10%空间,fillfactor 70意味着预留30%空间给未来的更新。fillfactor越低,HOT更新的概率越高。

四、fillfactor调优策略

fillfactor是控制HOT概率的唯一直接杠杆。设置策略取决于表的更新频率和行大小。

场景fillfactor预留空间说明
只读或极少更新1000%默认值,最大化存储密度
偶尔更新9010%轻微预留,几乎不影响存储密度
频繁更新70-8020-30%显著提高HOT概率
极频繁更新50-7030-50%更新频率极高的表

注意:fillfactor只对新建表或执行VACUUM FULL/CLUSTER后的表生效。对已有表修改fillfactor后,需要重建表才能生效:

ALTER TABLE orders SET (fillfactor = 80); VACUUM FULL orders;

五、监控HOT是否生效

SELECT schemaname, relname, n_tup_upd, n_tup_hot_upd, CASE WHEN n_tup_upd > 0 THEN ROUND(n_tup_hot_upd::NUMERIC / n_tup_upd * 100, 2) ELSE NULL END AS hot_pct FROM pg_stat_all_tables WHERE relname IN ('orders', 'users') ORDER BY n_tup_upd DESC;

判断标准:间隔一段时间(如1小时)对比两个快照中n_tup_upd和n_tup_hot_upd的增长量。如果两者增长量接近,说明几乎所有UPDATE都是HOT的;如果n_tup_hot_upd增长量远小于n_tup_upd,说明HOT没有生效。

一个邮件列表中的真实数据:某表n_tup_upd = 5,997,455,764,n_tup_hot_upd = 3,752,865,557,HOT比例约62.6%。说明约37%的更新没有走HOT,索引膨胀的速度远超预期。

六、小结

HOT更新是PostgreSQL控制索引膨胀的核心机制。当UPDATE不修改索引列且新行能放在同一页时,索引不需要新增条目,膨胀速度大幅降低。在频繁更新的列上建索引、使用表达式索引、fillfactor=100——这三个场景都会破坏HOT。fillfactor是唯一的直接调优杠杆,频繁更新的表应设为70-80。监控n_tup_hot_upd / n_tup_upd的比值,是判断HOT是否生效的最直接方法。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 8:50:46

商业利益冲突的事实核验、时间线与证据链为何延迟,怎样预防?

商业利益冲突核验的周期由问题范围、来源响应、主体匹配、异常补证和复核共同决定。企业应按阶段管理起算、暂停、升级、截止时点和版本更新,不能用一个平均天数掩盖证据质量。商业利益冲突核验容易延迟,通常不是调查人员“查得慢”,而是核验…

作者头像 李华
网站建设 2026/10/14 8:50:26

Python+Django构建校园舆情监测系统实战

1. 项目概述 校园舆情管理系统是一个基于PythonDjango框架开发的Web应用,主要用于监测和分析大学生在微博平台上的言论倾向。作为一名长期从事校园信息化建设的开发者,我发现在当前高校管理工作中,如何及时掌握学生群体的舆情动态是一个亟待解…

作者头像 李华
网站建设 2026/10/14 8:50:08

CS2闪退帧跳原因与四层穿透式排查法

1. 这不是游戏问题,是系统级协同失配的典型症状 “CS2 更新后疯狂闪退、弹溢出错误、帧数跳水”——这句标题我上周在三个不同技术群被了十七次,每次点开都是同一张蓝底白字的错误截图,右下角还带着Windows任务栏时间戳。它根本不是一句抱怨&…

作者头像 李华
网站建设 2026/10/14 8:50:04

2026跨端开发方案横评:从人力成本与框架选型看效率之王

2026年聊跨端方案,已经不是“选哪个框架”这么简单了,而是“在人力成本一路走高的压力下,你的团队还能不能撑住多端并行交付”。App、小程序、H5、桌面端,每个入口都想要,但每多一个端,就意味着多一套语言、…

作者头像 李华
网站建设 2026/10/14 8:50:02

Hue与Impala时间格式解析异常问题解决方案

1. 问题背景与现象描述最近在升级Hue到4.11.0版本后,发现与Impala 4.4.1交互时出现了时间格式解析异常的问题。具体表现为:当通过Hue界面执行包含TIMESTAMP类型字段的Impala查询时,返回结果中的时间显示格式与升级前存在明显差异,…

作者头像 李华