📌 一句话总结
HOT = UPDATE 时不用改索引,性能提升 60%+
🏠 生活中的类比:公寓楼管理
场景设定
想象你是一栋公寓楼的管家,每个房间住一个人(数据行),你有本住户名册(索引)。
❌ 没有 HOT 的情况(fillfactor=100)
📦 1号楼(数据页)- 已住满
┌──────────────┐
│ 101: 张三 │ ← 名册记录:张三在 1号楼101
│ 102: 李四 │
│ 103: 王五 │
│ 104: 赵六 │
│ 105: 钱七 │ ← 100% 住满,没空房
└──────────────┘
现在张三结婚了,要改成"张三(已婚)"
❌ 1号楼没空房,无法原地修改
✅ 只能让张三搬到 2号楼
操作步骤:
1. 在 2号楼 给张三分配新房间
2. 1号楼101 标记为"已搬走"(垃圾数据)
3. 🔴 修改名册:把"张三在 1号楼101" 改成 "张三在 2号楼101"
4. 如果有 5 本名册(5个索引),就要改 5 次!
开销:大!🚫
- 要写 2 个楼的数据
- 要改 5 本名册
- 要记录详细的搬迁日志(WAL)
✅ 有 HOT 的情况(fillfactor=75)
📦 1号楼(数据页)- 预留空房
┌──────────────┐
│ 101: 张三 │ ← 名册记录:张三在 1号楼101
│ 102: 李四 │
│ 103: 王五 │
│ 104: 赵六 │
│ ⬜ 空房 │ ← 预留 25% 空房
│ ⬜ 空房 │
└──────────────┘
现在张三结婚了,要改成"张三(已婚)"
✅ 1号楼有空房,可以原地修改!
操作步骤:
1. 在 1号楼的空房给张三安排新房间
2. 101房间放个指示牌:"我搬到 105了"(HOT 链)
3. 🟢 名册不用改!仍写着"张三在 1号楼101"
4. 查名册时:101 → 看到指示牌 → 找到 105
开销:小!✅
- 只写 1 个楼的数据
- 名册完全不用改(省了 5 次修改)
- 日志量少很多
🔍 技术原理(简化版)
什么是 HOT?
HOT (Heap Only Tuple) = PostgreSQL 的一种聪明做法
传统 UPDATE:
索引 → 旧数据(死元组)
索引 → 新数据(需要更新索引)
HOT UPDATE:
索引 → 旧数据 → HOT链 → 新数据(索引不用改)
HOT 链是什么?
就像一个"接力棒":
索引指向:Page1.Row1(旧数据)
Page1.Row1 内部有个指针:t_ctid → Page1.Row5(新数据)
查询时自动沿着指针找到最新数据,用户无感知
🎯 HOT 生效的条件
必须同时满足 3 个条件:
| 条件 | 说明 | 检查方法 |
|---|---|---|
| 1️⃣ 页面有空闲空间 | fillfactor < 100 | SELECT reloptions FROM pg_class |
| 2️⃣ 没改索引列 | UPDATE 的字段不在任何索引中 | 查看表的索引定义 |
| 3️⃣ 空间够用 | 新数据能放进空闲区 | 通常都满足 |
⚠️ 常见失败原因
sql
-- 情况 1:fillfactor=100(没预留空间)
ALTER TABLE my_table SET (fillfactor = 100); -- ❌ HOT 失效
-- 情况 2:更新了主键或索引列
UPDATE my_table SET id = 2 WHERE id = 1; -- ❌ id 是主键,HOT 失效
-- 情况 3:页面太满
-- 即使 fillfactor=75,如果历史数据已经占满,HOT 也会失效
-- 解决:VACUUM FULL 整理表
📊 性能对比
假设 1000 次 UPDATE 操作
| 指标 | 无 HOT | 有 HOT | 提升 |
|---|---|---|---|
| 索引更新次数 | 5000 次 | 500 次 | 📉 90% |
| WAL 日志量 | 100 MB | 40 MB | 📉 60% |
| 磁盘 I/O | 10000 次 | 4000 次 | 📉 60% |
| 执行时间 | 100 秒 | 40 秒 | 🚀 60% |
| 并发能力 | 低(锁竞争) | 高 | 🚀 2-3倍 |
🔧 如何启用 HOT 优化
步骤 1:设置填充因子
sql
-- 设置为 75%,预留 25% 空间给 UPDATE
ALTER TABLE magellan_sell_through SET (fillfactor = 75);
步骤 2:重新整理表(必须!)
sql
-- 已有数据不会自动调整,需要重建
VACUUM FULL magellan_sell_through;
-- 注意:这会锁表,建议在维护窗口执行
步骤 3:验证 HOT 效果
sql
-- 查看 HOT 命中率
SELECT
relname AS 表名,
n_tup_upd AS 总更新次数,
n_tup_hot_upd AS HOT更新次数,
round(100.0 * n_tup_hot_upd / NULLIF(n_tup_upd, 0), 2) AS HOT命中率
FROM pg_stat_user_tables
WHERE relname = 'magellan_sell_through';
-- 期望结果:HOT命中率 > 70%
步骤 4:持续监控
sql
-- 每周检查一次
SELECT
schemaname,
relname,
seq_scan, -- 顺序扫描次数
idx_scan, -- 索引扫描次数
n_tup_ins, -- INSERT 次数
n_tup_upd, -- UPDATE 次数
n_tup_del, -- DELETE 次数
n_tup_hot_upd, -- HOT UPDATE 次数
last_vacuum, -- 上次 VACUUM 时间
last_autovacuum -- 上次自动 VACUUM 时间
FROM pg_stat_user_tables
ORDER BY n_tup_upd DESC;
💡 实际案例
你的项目场景
sql
-- 当前 SQL(已优化)
INSERT INTO magellan_sell_through (...)
SELECT ... FROM magellan_sell_through_inbound
ON CONFLICT (sell_through_record_id)
DO UPDATE SET
version_number = excluded.version_number,
released_dt = excluded.released_dt,
qty = excluded.qty,
-- ... 191 个字段
WHERE
magellan_sell_through.version_number IS DISTINCT FROM excluded.version_number
OR magellan_sell_through.qty IS DISTINCT FROM excluded.qty
-- ... 其他字段比较
优化效果预估
假设每天 100 万次写入:
优化前(无 WHERE + fillfactor=100):
- 100 万次完整 UPDATE
- 每次都要改索引、写 WAL
- 耗时:约 1000 秒
优化后(有 WHERE + fillfactor=75):
- 90 万次被 WHERE 过滤(不执行 UPDATE)
- 10 万次真实 UPDATE 中,7 万次通过 HOT 优化
- 耗时:约 200 秒
- 性能提升:80% 🚀
❓ 常见问题
Q1: fillfactor 设多少合适?
推荐值:
- 高频更新表:fillfactor = 70-75
- 中频更新表:fillfactor = 80-85
- 低频更新表:fillfactor = 90-95
- 只读表:fillfactor = 100(默认)
平衡点:75 是性能和空间的黄金比例
Q2: 会浪费多少空间?
举例:
- 表当前大小:100 GB
- fillfactor=100 → 实际占用 100 GB
- fillfactor=75 → 实际占用 133 GB(多占 33 GB)
但换来的是 60-80% 的性能提升,非常值得!
Q3: 什么时候 HOT 会失效?
常见原因:
1. 更新了索引列(主键、唯一索引等)
2. 页面没有空闲空间(fillfactor 设置不当或未 VACUUM)
3. 行太大,一页只能存几行
4. 表从未做过 VACUUM,死元组占满空间
解决方法:
- 避免更新索引列
- 设置合理的 fillfactor
- 定期 VACUUM 或启用 autovacuum
Q4: 怎么知道我的表有没有用到 HOT?
sql
-- 方法 1:查看统计信息
SELECT
n_tup_hot_upd / n_tup_upd AS hot_ratio
FROM pg_stat_user_tables
WHERE relname = 'your_table';
-- 方法 2:查看执行计划
EXPLAIN (ANALYZE, BUFFERS)
UPDATE your_table SET col1 = 'new' WHERE id = 1;
-- 观察输出中的 "Heap Blocks" 和 "Buffers"
Q5: fillfactor 改了之后,已有的数据怎么办?
重要:修改 fillfactor 不会影响已有数据!
必须执行:
VACUUM FULL table_name;
或者:
1. 创建新表
2. 复制数据
3. 删除旧表
4. 重命名新表
建议:在业务低峰期执行,因为会锁表
🎯 最佳实践总结
✅ 应该做的
-
高频更新表设置 fillfactor=75
sqlALTER TABLE hot_table SET (fillfactor = 75); VACUUM FULL hot_table; -
避免更新索引列
sql-- ❌ 不好 UPDATE table SET primary_key = 2 WHERE primary_key = 1; -- ✅ 好 UPDATE table SET non_index_column = 'new_value' WHERE id = 1; -
定期监控 HOT 命中率
sql-- 每周执行一次 SELECT relname, n_tup_hot_upd / n_tup_upd AS hot_ratio FROM pg_stat_user_tables WHERE hot_ratio < 0.5; -- 找出 HOT 命中率低的表 -
启用自动 VACUUM
sql-- PostgreSQL 默认已启用 SHOW autovacuum; -- 应该是 on -- 对于特别重要的表,可以调整参数 ALTER TABLE my_table SET ( autovacuum_vacuum_threshold = 1000, autovacuum_vacuum_scale_factor = 0.1 );
❌ 不应该做的
-
不要在生产环境随意 VACUUM FULL
- 会锁表,影响业务
- 建议在维护窗口执行
-
不要把 fillfactor 设得太低
- fillfactor=50 会浪费 50% 空间
- 性价比不高
-
不要忽略监控
- 定期检查 HOT 命中率
- 发现异常及时调整
📚 相关概念
填充因子(fillfactor)
- 控制数据页的填充百分比
- 预留空间给 UPDATE 操作
- 推荐值:70-75(高频更新表)
WAL(Write-Ahead Logging)
- PostgreSQL 的事务日志
- HOT 可以减少 WAL 日志量 60%+
VACUUM
- 清理死元组(垃圾数据)
- 回收空间,让 HOT 有机会生效
- 建议启用 autovacuum
死元组(Dead Tuple)
- UPDATE/DELETE 后留下的旧数据
- 需要 VACUUM 清理
- HOT 可以减少死元组的产生
🔗 参考资料
📝 总结
HOT 优化的核心价值:
让 UPDATE 操作在同一页内完成,避免修改索引,从而大幅提升性能。
实施步骤:
- 设置
fillfactor = 75 - 执行
VACUUM FULL - 监控 HOT 命中率(目标 > 70%)
- 配合 WHERE 条件减少无效更新
预期收益:
- 📉 WAL 日志减少 60%
- 📉 I/O 开销减少 60%
- 🚀 执行时间减少 60-80%
- 🚀 并发能力提升 2-3 倍
最后提醒:
HOT 不是银弹,但对于高频 UPDATE 的场景,它是性价比最高的优化手段之一!