删了却没变小:PostgreSQL 死元组与表膨胀排查

一张表 DELETE 了一大半,磁盘占用却纹丝不动;du 看文件没缩,监控里表大小还在涨。你开始怀疑 PostgreSQL 是不是"假的删"。其实不是------这是 MVCC 机制下的表膨胀 ,根子在**死元组(dead tuples)**没人清。

下面把"为什么删了不瘦、怎么查、怎么治"讲清楚,照着做就行。

先搞懂:为什么删了不瘦

PostgreSQL 用 MVCC:一条 UPDATE/DELETE 不会立刻抹掉旧数据,而是给旧版本打上"过期"标记,变成死元组 。这些死元组占着空间,普通 VACUUM 只是把它们标记成"可复用",文件并不会缩小------新数据能盖上去,但磁盘占用不变。

只有 VACUUM FULL 会重写整张表、真正回收空间,可它要拿 ACCESS EXCLUSIVE 锁,大表一跑就是停产几小时,还顺带把索引也废了要重建。所以"删了不瘦"正常,但要命的是死元组一直堆、没人清。

第 1 招:一眼看表胀没胀

|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_vacuum FROM pg_stat_user_tables WHERE n_dead_tup > 0 ORDER BY n_dead_tup DESC LIMIT 20; |

n_dead_tup / (n_dead_tup + n_live_tup) 这个比例超过 20% ,表就在明显膨胀,该管了。last_autovacuum 为空或很久以前,说明 autovacuum 要么没触发、要么跑得太慢。

第 2 招:算 autovacuum 的触发阈值

autovacuum 不是一有死元组就动,它要攒够才触发:

|-------------------------------------------------------------------------------------------|
| 触发阈值 = autovacuum_vacuum_threshold(默认 50) + autovacuum_vacuum_scale_factor(默认 0.2) × 活元组数 |

一张 1000 万行的表,要攒到约 200 万 死元组才触发------对高频更新表来说太迟钝,期间死元组一路堆。热点大表要把 scale_factor 调小:

|--------------------------------------------------------------------------------------------------------|
| ALTER TABLE big_table SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_threshold = 1000); |

第 3 招:查谁卡住了死元组清理

死元组能不能清,取决于最老的那个事务------只要有个长事务(或一直开着的"空闲事务")没提交,它之前版本的所有死元组谁都不敢动,膨胀越积越多。

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| SELECT pid, state, age(backend_xmin) AS xmin_age, now() - xact_start AS dur, query FROM pg_stat_activity WHERE state IS NOT NULL AND backend_xmin IS NOT NULL ORDER BY xmin_age DESC LIMIT 10; |

看到 state='idle in transaction' 或跑了很久还没提交的,尽快让它提交或 pg_terminate_backend(pid) 杀掉。

第 4 招:全局把 autovacuum 调得更勤快

单表调完,还可以让全局更激进一点:

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ALTER SYSTEM SET autovacuum_max_workers = 6; -- 默认 3,并发清更多表 ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 2000; -- 默认 200,清得快但占 IO ALTER SYSTEM SET autovacuum_vacuum_cost_delay = 10; -- 默认 2ms,调小更积极 SELECT pg_reload_conf(); |

IO 吃紧的环境别一下调太狠,cost_limit 慢慢加。

第 5 招:在线回收空间,别用 VACUUM FULL

真要回收物理空间、又不想锁表停产,用 `pg_repack`(需单独装扩展):它建一份新表边写边切换,只拿很短的锁,大表也能在线重排。

|---------------------------------------------------------------------------------------------------------------------------|
| # 装好扩展后,对单表在线重排 pg_repack -t big_table -d mydb |

相比之下 VACUUM FULL 全表重写 + 长锁 + 索引重建,大表慎用。

表膨胀排查六步

1. pg_stat_user_tables 看 n_dead_tup、比例、last_autovacuum;

**2.**比例超 20% 记为待治理表;

3. pg_stat_activity 找长事务 / 空闲事务,挡住死元组清理;

4. ALTER TABLE 按表收紧 scale_factor 到 0.01~0.02;

5. 全局 autovacuum_max_workers/cost_limit 调高、cost_delay 调小;

6. 在线回收用 pg_repack,别用 VACUUM FULL 停产。

写在最后

表膨胀不是 PostgreSQL 的 bug,是 MVCC 的代价。"删了不瘦"正常,真正要治的是死元组堆积 ------先解决挡路的长事务,再把热点表的 autovacuum 阈值收紧,全局调勤快,空间回收交给 pg_repack。

监控别只看表大小,把 n_dead_tup 比例和 last_autovacuum 时间也挂上告警,膨胀刚冒头就能看到,不用等盘快满才手忙脚乱。

相关推荐
SelectDB35 分钟前
让变更流出 Doris:Apache Doris 5.0 基于增量计算的 CDC 技术解析
数据库
这个DBA有点耶44 分钟前
MCP隔离层+行级语义审计+输出校验:AI访问数据库的安全边界拆解
数据库·人工智能·ai编程
这个DBA有点耶2 小时前
从反向扫描O(n)到正向扫描O(1):降序索引的物理机制与混合排序匹配规则
数据库
2501_933923253 小时前
解读Redis:单线程模型
数据库·redis·缓存
Tapdata4 小时前
被忽略的第三类上下文:Agent 进入真实业务后,还缺什么?
数据库·人工智能·架构
小科先生5 小时前
Java springboot调用数据库的基本流程
java·数据库·spring boot
星恒随风5 小时前
MySQL 从零入门(四):表的增删改查与基础查询详解
linux·服务器·数据库·笔记·mysql
Pniubi7 小时前
MySQL靠什么实现事务的?事务隔离级别有哪些?
java·数据库·mysql
数据库小学妹8 小时前
pgbouncer 事务池:prepared statement、临时表与会话状态
数据库·后端·postgresql
Lucifer三思而后行9 小时前
最近折腾了一下金仓数据库的备份恢复
数据库·备份恢复