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

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


相关推荐
闪电悠米3 分钟前
黑马点评-优惠券秒杀-03_basic_seckill_and_oversell
java·数据库·spring boot·spring·缓存·oracle·面试
逍遥德5 分钟前
PostgreSQL --- 数组函数详解
数据库·sql·postgresql
.Cnn5 分钟前
MySQL事务和Spring事务
数据库·后端·mysql·spring
福大大架构师每日一题10 分钟前
redis 8.8.0 发布:新数据结构、字段级通知、INCREX、XNACK 全面升级,8.6 到 8.8 变化一文看懂
数据结构·数据库·redis
霸道流氓气质11 分钟前
Spring Data JPA 完全指南
开发语言·数据库
Demon1_Coder13 分钟前
Day4-LangChain4j-向量数据库-检索增强RAG
数据库
phltxy13 分钟前
RabbitMQ 应用问题
数据库·分布式·rabbitmq
星晨雪海14 分钟前
基于 SpringBoot + Redis (Lettuce) + RabbitMQ 实现「Redis 预扣库存 + 异步同步数据库」
数据库·spring boot·java-rabbitmq
mosaic_born15 分钟前
centos 7.9 离线部署Zabbix 6.0.46 监控详细方案(解决数据库字符集问题)
数据库·centos·zabbix
weelinking16 分钟前
【产品】10_搭建前端框架——把你的原型变成真实页面
java·大数据·前端·数据库·人工智能·python·前端框架