一张表 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 时间也挂上告警,膨胀刚冒头就能看到,不用等盘快满才手忙脚乱。