PostgreSQL HOT 优化 - 大白话解释

📌 一句话总结

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. 重命名新表

建议:在业务低峰期执行,因为会锁表

🎯 最佳实践总结

✅ 应该做的

  1. 高频更新表设置 fillfactor=75

    sql 复制代码
    ALTER TABLE hot_table SET (fillfactor = 75);
    VACUUM FULL hot_table;
  2. 避免更新索引列

    sql 复制代码
    -- ❌ 不好
    UPDATE table SET primary_key = 2 WHERE primary_key = 1;
    
    -- ✅ 好
    UPDATE table SET non_index_column = 'new_value' WHERE id = 1;
  3. 定期监控 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 命中率低的表
  4. 启用自动 VACUUM

    sql 复制代码
    -- PostgreSQL 默认已启用
    SHOW autovacuum;  -- 应该是 on
    
    -- 对于特别重要的表,可以调整参数
    ALTER TABLE my_table SET (
        autovacuum_vacuum_threshold = 1000,
        autovacuum_vacuum_scale_factor = 0.1
    );

❌ 不应该做的

  1. 不要在生产环境随意 VACUUM FULL

    • 会锁表,影响业务
    • 建议在维护窗口执行
  2. 不要把 fillfactor 设得太低

    • fillfactor=50 会浪费 50% 空间
    • 性价比不高
  3. 不要忽略监控

    • 定期检查 HOT 命中率
    • 发现异常及时调整

📚 相关概念

填充因子(fillfactor)

  • 控制数据页的填充百分比
  • 预留空间给 UPDATE 操作
  • 推荐值:70-75(高频更新表)

WAL(Write-Ahead Logging)

  • PostgreSQL 的事务日志
  • HOT 可以减少 WAL 日志量 60%+

VACUUM

  • 清理死元组(垃圾数据)
  • 回收空间,让 HOT 有机会生效
  • 建议启用 autovacuum

死元组(Dead Tuple)

  • UPDATE/DELETE 后留下的旧数据
  • 需要 VACUUM 清理
  • HOT 可以减少死元组的产生

🔗 参考资料


📝 总结

HOT 优化的核心价值:

让 UPDATE 操作在同一页内完成,避免修改索引,从而大幅提升性能。

实施步骤:

  1. 设置 fillfactor = 75
  2. 执行 VACUUM FULL
  3. 监控 HOT 命中率(目标 > 70%)
  4. 配合 WHERE 条件减少无效更新

预期收益:

  • 📉 WAL 日志减少 60%
  • 📉 I/O 开销减少 60%
  • 🚀 执行时间减少 60-80%
  • 🚀 并发能力提升 2-3 倍

最后提醒:

HOT 不是银弹,但对于高频 UPDATE 的场景,它是性价比最高的优化手段之一!

相关推荐
Boop_wu2 小时前
[Java EE进阶] 博客系统
数据库·sql
Juicedata2 小时前
JuiceFS 1.4|大规模元数据操作优化:批量删除、克隆与 Redis 缓存全解析
数据库·redis·缓存
这个DBA有点耶2 小时前
SQL改写实战(续):子查询vs JOIN的深层原理
数据库·sql
yyuuuzz2 小时前
独立站搭建的几个核心技术问题
运维·服务器·网络·数据库·aws
小蒋学算法2 小时前
redis分布式锁实现
数据库·redis·分布式
白菜欣2 小时前
【MySQL】MySQL数据的增删改查(入门版)
数据库·mysql
unicorn312 小时前
r-pan
数据库
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第97题】【Mysql篇】第27题:说说分库与分表的设计?
java·开发语言·数据库·分布式·mysql·算法