InnoDB的DELETE仅逻辑标记删除,不释放磁盘空间;必须通过OPTIMIZE TABLE或ALTER TABLE ENGINE=InnoDB重建表才能真正释放.ibd文件空间。

MySQL 删除百万行后磁盘空间不释放、查询反而变慢,不是配置错了,也不是服务器坏了,而是 InnoDB 的 DELETE 本质就是逻辑标记,不是物理擦除——它把行标为“已删除”,但页(16KB)仍占着 .ibd 文件位置,空洞堆在那里,后续查询要扫更多页,I/O 就上去了。
DELETE 后 Data_length 不变是正常现象,不是 bug
执行 SHOW TABLE STATUS LIKE 't' 时,Data_length 字段反映的是数据页总占用量,不是“有效数据”大小。InnoDB 不会因 DELETE 主动归还页给操作系统,只把被删记录加入内部空闲链表(free list),供后续 INSERT 复用。所以:
-
SELECT COUNT(*)返回 0,但Data_length和删前几乎一样 -
du -sh table.ibd看到的文件大小纹丝不动 -
Data_free值可能反而增大——说明碎片加剧,空洞变多
查询变慢的根因是数据空洞导致 I/O 放大
MySQL 每次读取以页为单位,一个满页能存 100 行,但若页里只有 20 行有效数据 + 80 行“墓碑”,那查 100 行就得读 5 个页,而不是 1 个。尤其当表经历过大量随机 DELETE/UPDATE,B+ 树叶子节点稀疏、跨页跳转频繁,EXPLAIN 中 rows 显示扫描行数暴增,实际磁盘读次数翻倍。这不是索引失效,是物理布局崩了。
- 典型表现:
WHERE service = 'xxx'查询,删完数据后rows从 1000 跳到 800 万 - 即使加了
service索引,二级索引叶子节点指向的聚簇索引页里,大量位置是空洞,回表成本飙升 -
innodb_file_per_table = ON是前提,否则所有表共享ibdata1,OPTIMIZE TABLE无效
OPTIMIZE TABLE 才是真正释放空间和修复查询的关键操作
OPTIMIZE TABLE t 在 InnoDB 中等价于 ALTER TABLE t ENGINE=InnoDB,它会新建空页、逐行拷贝有效数据、丢弃旧文件,从而合并空洞、重建 B+ 树、收缩 .ibd。但必须注意:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- MySQL 5.7 及以前会全程加表级锁;8.0 默认用
ALGORITHM=INPLACE,但仍需短暂排他锁(metadata lock),避开业务高峰 - 操作期间磁盘空间需有余量:新旧表同时存在,临时空间至少要 >= 原表
Data_length - 该操作不写 binlog row event(除非显式开启
log_bin),主从.ibd文件大小可能不一致 - 别在
df -h显示磁盘 >90% 时硬上,先扩容,再优化,完事缩容
真正线上安全的清理流程不是单条 DELETE,而是分批 + 重建组合
直接 DELETE FROM t WHERE created_at 删百万行,会触发长事务、撑爆 undo log、拉高主从延迟、阻塞 purge 线程。稳妥做法是:
- 先按主键或自增 ID 分批删,每批控制在 1~5 万行,用
WHERE id BETWEEN ? AND ?避免全表扫描 - 每批提交后 sleep 100~500ms,缓解复制压力和锁竞争
- 全部删完后,立刻执行
OPTIMIZE TABLE t或ALTER TABLE t ENGINE=InnoDB - 如果不能停写,可用
pt-online-schema-change替换表(但大表耗时更长,需评估)
最易被忽略的一点:删完不重建,等于白删——空洞还在,查询照慢,磁盘照满。而重建本身又是个重 I/O 操作,必须提前规划窗口期和磁盘余量。










