一、一个让人误解多年的问题
很多人刚接触 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 的核心本质。
下一步建议你做两件事:
-
查一下你线上表的
n_tup_hot_upd -
看看哪些 UPDATE 本可以变成 HOT
你会发现性能空间远比你想的大。