删了却没变小: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 时间也挂上告警,膨胀刚冒头就能看到,不用等盘快满才手忙脚乱。

相关推荐
依晨照旧1 小时前
Langfuse 打爆两台机器:一条分位数 SQL,在 400G 表上扫了 8 分钟
数据库
笃行3502 小时前
列存储这条线,我顺着 KingbaseES 的文档摸了一遍
数据库
Navicat中国2 小时前
Navicat 18 支持 Azure OpenAI 接入,合规 AI 赋能 SQL 开发
数据库·人工智能·microsoft·ai·azure·navicat·数据
1024奇点2 小时前
【仓颉语言入门 · 第二篇 第 10 课】
开发语言·数据库·ide·mysql·开源
数据狐(Datafox)2 小时前
一文读懂京东商品详情API:返回字段解读与 JSON 样例
java·开发语言·数据库·windows·json
SelectDB3 小时前
从实时入库到实时可用:Apache Doris 5.0 增量计算场景与实践
大数据·数据库·开源
IvorySQL3 小时前
PostgreSQL 18 性能与执行计划新特性实测
数据库·postgresql
赵渝强老师3 小时前
【赵渝强老师】高斯数据库(openGauss)的服务器日志文件
数据库·opengauss·国产数据库·高斯数据库
SelectDB3 小时前
以实时驱动智能:Apache Doris Summit 2026 盛大落幕,开启 Agentic AI 时代数据新范式
数据库·程序员·agent