PostgreSQL 中 DELETE 和 UPDATE 不是物理删除或原地修改,而是标记死亡元组并插入新版本,导致表和索引持续膨胀、I/O 剧增、查询变慢;VACUUM 仅复用空间,VACUUM FULL 才缩表但会阻塞全部读写。

PostgreSQL 中 DELETE 不等于物理删除
DELETE 只是把行标记为“死亡元组”(dead tuple),不释放磁盘空间。MVCC 机制要求旧版本必须保留,直到所有可能用到它的事务结束。这意味着:即使你删掉 90% 的数据,表文件大小几乎不变,死元组仍占满页面。
- 每个 DELETE 操作都会在页内留下空洞,后续 INSERT 优先填这些空洞,但不会跨页合并
- 如果并发事务多、长事务存在,死元组会长期滞留——autovacuum 来不及清理
-
VACUUM只回收空间供本表复用,VACUUM FULL才真正缩表,但它会加AccessExclusiveLock,阻塞所有读写
UPDATE 实际是 DELETE + INSERT
PostgreSQL 的 UPDATE 并非原地修改,而是插入新版本元组,并把旧版本打上删除标记。高频同步场景(如每小时一次用户权限更新)会让同一行反复生成新版本,旧版本堆积成山。
- 一次
UPDATE= 1 个新元组 + 1 个死元组,空间占用翻倍 - 若该表有多个索引,每个索引都要分别维护新旧版本,索引也同步膨胀
- 查询时顺序扫描仍需跳过大量死元组,I/O 和 CPU 开销剧增——哪怕表里只剩 1600 行,4GB 大小也能让
SELECT耗时 150 秒
批量 DELETE + INSERT 组合更危险
显式写 DELETE 再跟 INSERT,不是“替换”,而是两次独立 DML:先制造一批死元组,再插入全新元组。中间还有“数据真空期”,且锁持有时间翻倍。
- 两轮索引操作:DELETE 走一遍唯一索引查找并标记删除;INSERT 再走一遍索引查找并插入
- InnoDB 或 PostgreSQL 都会为这两步分别加行级锁或间隙锁,高并发下极易锁等待甚至死锁
- 若没包在事务里,可能出现 DELETE 成功但 INSERT 失败,导致数据丢失
膨胀的连锁反应不止在表本身
频繁 DML 还会拖垮系统目录和索引结构。比如高频 DROP/CREATE TABLE 会让 pg_class、pg_attribute 等系统表快速堆满死元组,元数据查询(\d、information_schema)变慢,甚至影响整个集群的解析性能。
- 列存表(如 DWS)对
UPDATE/DELETE更敏感,会导致 CU 膨胀,访问路径退化 - 向量索引等特殊索引通常不支持 vacuum,DML 后只能重建,否则持续膨胀
- 填充因子(
fillfactor)设得过高或过低,都会加剧页分裂与空洞残留
VACUUM FULL 期间业务彻底不可用——这不是优化问题,是设计阶段就该规避的陷阱。

















