PostgreSQL 深入heap_update() 与 HOT 机制(附源码级解析)

一、一个让人误解多年的问题

很多人刚接触 PostgreSQL 时都会问:

❓ 为什么我只是更新一列,IO 却这么高?

比如这条 SQL:

复制代码
UPDATE user SET last_login = now() WHERE id = 1;

直觉上只是改一个字段,但实际情况是:

💥 PostgreSQL 会生成一条"新行",而不是原地更新!

更关键的是:

  • 有时候会更新索引

  • 有时候不会

  • 有时候性能很好(HOT)

  • 有时候性能很差(非 HOT)

这背后到底发生了什么?

本文直接带你从源码层拆解 heap_update(),彻底理解:

  • UPDATE 为什么会写整行

  • HOT update 是怎么触发的

  • 什么情况下索引会被更新

  • 如何设计高性能更新表


二、先说结论(建议先记住)

✔️ PostgreSQL 的 UPDATE = 一定生成新行(MVCC)

✔️ 是否更新索引,取决于是否能触发 HOT

✔️ HOT 的本质:

👉 不改索引 + heap 内部建立版本链


三、UPDATE 的本质:不是"改",而是"插"

在 PostgreSQL 中:

复制代码
UPDATE ≈ DELETE + INSERT(逻辑语义)

但内部更精确的说法是:

  • 旧行 → 标记为旧版本

  • 新行 → 插入一个新 tuple

  • 两者通过 t_ctid 连接

你可以用这个 SQL 验证:

复制代码
SELECT ctid, * FROM user WHERE id = 1;

UPDATE user SET last_login = now() WHERE id = 1;

SELECT ctid, * FROM user WHERE id = 1;

👉 如果 ctid 变了,说明已经是新行。


四、核心源码入口:heap_update()

PostgreSQL 所有 UPDATE 最终都会走到:

复制代码
heap_update(...)

核心流程可以简化为:

复制代码
heap_update()
{
    1. 找到旧 tuple
    2. 构造新 tuple
    3. 判断哪些列发生变化
    4. 判断是否影响索引
    5. 判断是否能写回同一 page
    6. 决定是否 HOT
    7. 写入新 tuple + 更新链
}

五、HOT 判断的核心逻辑(最关键)

HOT(Heap-Only Tuple)触发条件:

复制代码
1. 更新的列不影响任何索引
2. 新 tuple 能放回原 page

源码中核心判断可以抽象成:

复制代码
if (!bms_overlap(modified_attrs, hot_attrs) && same_page)
    use_hot_update = true;
else
    use_hot_update = false;

解释一下两个关键变量:


5.1 modified_attrs(哪些列变了)

通过:

复制代码
HeapDetermineColumnsInfo(...)

计算:

  • old tuple vs new tuple

  • 哪些列真的发生变化

👉 注意:不是 SQL 写了哪个列,而是"值是否真的变了"。


5.2 hot_attrs(哪些列会阻止 HOT)

通过:

复制代码
RelationGetIndexAttrBitmap(...)

得到:

👉 所有被索引依赖的列

包括:

  • 普通索引列

  • 表达式索引

  • 部分索引条件列


🔥 关键一句话

❗ 只要你更新的列"可能影响任何索引",就不能 HOT


六、HOT 成功后发生了什么?

如果满足条件:

✔️ 新 tuple

  • 标记:HEAP_ONLY_TUPLE

  • 不进索引


✔️ 旧 tuple

  • 标记:HEAP_HOT_UPDATED

  • t_ctid 指向新 tuple


✔️ 索引

👉 完全不变!

结构变成:

复制代码
index → old tuple → new tuple → new tuple

七、非 HOT 时会发生什么?

如果不能 HOT:

  • 新 tuple 插入 ✔️

  • 所有相关索引插入新 entry ✔️

  • 旧索引 entry 保留(等待 VACUUM)

👉 这就是性能差异的来源!


八、为什么 HOT 必须"同一 page"?

很多人忽略这一点。

原因是:

索引仍然指向旧 tuple,如果新 tuple 不在同一 page,就无法高效跟踪版本链

所以 PostgreSQL 强制:

复制代码
HOT update 必须 page 内完成

九、影响 HOT 成功率的 4 个关键因素

更新列是否被索引

👉 最关键


fillfactor

复制代码
ALTER TABLE t SET (fillfactor = 70);

👉 留空间给 UPDATE


行大小

  • 行越大,越难 HOT

  • TOAST 字段影响很大


page 是否已满

👉 最常见 HOT 失败原因


十、实战优化建议(非常重要)

1. 热字段拆表(强烈推荐)

复制代码
-- 主表(低频更新)
user_profile

-- 热表(高频更新)
user_profile_hot

2. 高频更新列不要建索引

复制代码
否则 HOT 永远失败 ❌

3. 设置 fillfactor

复制代码
ALTER TABLE user_profile_hot SET (fillfactor = 70);

4. 避免无效 UPDATE

复制代码
UPDATE t
SET status = 1
WHERE id = 1
  AND status IS DISTINCT FROM 1;

5. 监控 HOT 命中率

复制代码
SELECT
    relname,
    n_tup_upd,
    n_tup_hot_upd
FROM pg_stat_user_tables;

十一、一句话总结

💡 PostgreSQL UPDATE 的性能关键,不在"你改了多少列",而在:

👉 你改的列是否影响索引 + 能不能在同一 page 完成更新


结尾

如果你读到这里,说明你已经理解了 PostgreSQL UPDATE 的核心本质。

下一步建议你做两件事:

  1. 查一下你线上表的 n_tup_hot_upd

  2. 看看哪些 UPDATE 本可以变成 HOT

你会发现性能空间远比你想的大。


相关推荐
qq_342295822 小时前
如何为容器内多个列表实现统一滚动条.txt
jvm·数据库·python
qq_206901393 小时前
CSS如何引入自适应图标_利用svg外链配合css控制颜色
jvm·数据库·python
weixin_408717773 小时前
Go语言怎么编译Linux程序_Go语言编译Linux可执行文件教程【避坑】
jvm·数据库·python
APIshop3 小时前
Python 爬虫获取京东商品详情 API 接口实战指南
java·服务器·数据库
XmasWu12253 小时前
【Hermes Agent进阶】开发自定义技能
网络·数据库
刘~浪地球4 小时前
数据库性能优化实战
数据库·性能优化
2401_865439634 小时前
CSS怎么在flex布局中实现项目均分间距_设置justify-content space-evenly
jvm·数据库·python
m0_493934534 小时前
CSS如何实现输入框禁用样式_使用-disabled伪类设定
jvm·数据库·python