PostgreSQL 中的死元组(Dead tuple)详解
作者:ShiningStar_Li
为什么会有死元组?(MVCC机制)
PostgreSQL 使用 MVCC(多版本并发控制) 来实现高并发和事务隔离。这与 MySQL (InnoDB) 的机制有很大不同。
当你执行 UPDATE 时:
- PG 不会直接修改原来的那一行数据。
- PG 会将旧行标记为“死亡”(设置
xmax事务ID)。 - PG 会插入一个全新的行,包含更新后的数据。
- 结果:表中同时存在“旧版本(死元组)”和“新版本(活元组)”。
当你执行 DELETE 时:
- PG 不会直接从磁盘移除该行。
- PG 只是将该行标记为“死亡”(设置
xmax事务ID)。 - 结果:该行变成了死元组,依然占据磁盘空间。
为什么要这样做?为了事务隔离。
假设事务 A 正在读取某行数据,此时事务 B 更新了这行数据。
- 如果 PG 直接覆盖了旧数据,事务 A 就会读到不一致的数据(违反了隔离性)。
- 通过保留旧版本(死元组),事务 A 可以继续读取它启动时看到的那个版本,而事务 B 读取新版本。互不干扰。
死元组的危害
如果死元组不及时清理,会带来严重后果:
表膨胀(Table Bloat):
- 表文件越来越大,即使你只存了 1GB 的有效数据,表文件可能高达 10GB(全是死元组)。
- 导致磁盘空间浪费。
查询性能下降:
- 扫描表(Seq Scan)或索引时,PG 必须跳过大量的死元组。
- 索引也会变得庞大且碎片化,降低索引效率。
事务 ID 回卷风险(Transaction ID Wraparound):
- PG 使用 32 位整数存储事务 ID。如果死元组不被清理(冻结),事务 ID 用完后会发生回卷,导致数据丢失且数据库无法启动。这是 PG 最严重的故障之一。
死元组何时被清理?
死元组不会自动消失,必须由 VACUUM 进程来清理。
清理的条件(Visibility Map)
VACUUM 只能清理那些对所有当前活跃事务都不可见的死元组。
- 如果有一个长事务(Long-running Transaction)还在运行,并且它可能需要看到某个旧版本的数据,那么 VACUUM 不能删除那个死元组。
- 这就是你之前遇到
dead row versions cannot be removed yet的原因。
清理的过程
- 标记可用:VACUUM 将死元组占用的空间标记为“空闲”,供后续的
INSERT或UPDATE复用。 - 更新可见性映射(Visibility Map):记录哪些页面完全由活元组组成,加速后续查询。
- 冻结事务 ID:防止事务 ID 回卷。
注意:普通的 VACUUM 不会缩小表文件的大小(不会归还空间给操作系统),它只是让空间在内部可复用。只有 VACUUM FULL 才会真正缩小文件。
如何监控死元组?
你可以查询系统视图 pg_stat_user_tables 来查看表的死元组情况:
SELECT
relname AS table_name,
n_live_tup AS live_tuples, -- 活元组数量
n_dead_tup AS dead_tuples, -- 死元组数量
last_vacuum, -- 上次手动 vacuum 时间
last_autovacuum, -- 上次自动 vacuum 时间
last_autoanalyze -- 上次自动 analyze 时间
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;- 健康状态:
n_dead_tup应该接近 0,或者远小于n_live_tup。 - 危险信号:如果
n_dead_tup很大(例如几百万),且last_autovacuum是很久以前,说明 Autovacuum 失效或被阻塞,表正在严重膨胀。
如何处理过多的死元组?
依赖 Autovacuum(默认推荐)
PostgreSQL 有一个后台进程叫 autovacuum,它会自动检测表的变动并触发 VACUUM。
调优建议:对于高频更新的表,可以单独调整其存储参数:
ALTER TABLE ais41.recentships SET (autovacuum_vacuum_scale_factor = 0.05);
(默认是 0.2,即 20% 的死元组才触发;改为 0.05 表示 5% 就触发,更频繁地清理。)
杀死长事务
如前所述,长事务会阻止 VACUUM 清理死元组。
查找并终止长时间运行的事务:
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state != 'idle' AND now() - xact_start > interval '10 minutes';
手动 VACUUM
如果 Autovacuum 跟不上节奏,可以手动执行:
VACUUM VERBOSE ais41.recentships;
重建表(极端情况)
如果表已经极度膨胀(例如 90% 都是死元组),普通 VACUUM 效率太低。
- 使用
VACUUM FULL(锁表,慎用)。 - 或使用
pg_repack插件(在线重组,推荐生产环境使用)。
到此这篇关于PostgreSQL 中的死元组(Dead tuple)详解的文章就介绍到这了,更多相关PostgreSQL 死元组内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
