MySQL 8.0在架构层面做了不少改动,其中与存储最相关的就是数据字典下沉到InnoDB以及原子DDL的引入。很多实例从5.7迁移上来之后,运维人员注意到同样的业务数据,表空间ibd文件占用的字节数变多了,而且日常delete或者批量清理历史数据以后,操作系统层面看到的文件大小纹丝不动。这种现象并不是升级失败,而是InnoDB对空闲页的回收策略发生了变化。在8.0里,当一行记录被删除,页内只是被打上删除标记,随后由purge线程异步清理,但对应的页如果没能完全清空,就不会立刻归还给表空间的文件系统层,久而久之就形成了碎片。

除了删除操作,线上常见的热点更新也会放大碎片问题。比如一个宽表频繁更新变长字段,旧版本记录被标记删除,新记录写入其他页,导致原本连续的页变得稀疏。在MySQL 5.7时代,某些场景下可以通过innodb_file_per_table配合小批量optimize来收敛,但8.0由于原子DDL要保证回滚能力,重建过程的元数据锁粒度更粗,碎片堆积如果放任不管,不仅浪费磁盘,还会让缓冲池命中率下降,因为同样的数据分散在更多页中。
MySQL 8.0表空间碎片产生的底层原因
InnoDB的表空间由多个页组成,默认页大小16KB,页又组织成区。当执行delete语句时,记录并没有立刻从页中物理抹除,而是写入回滚段并由purge线程后续处理。在MySQL 8.0里,数据字典自身也使用了InnoDB表存储,原子DDL要求DDL操作要么完全成功要么完全回滚,这让空间回收必须更谨慎。一个页如果只删除了其中几行,剩余行仍在使用,这个页就不会被释放,文件系统看到的ibd文件自然不会缩小。
另一个容易被忽略的点是碎片来自页分裂。当主键或二级索引的插入顺序不连续,B+树节点满后会分裂出新的页,老页留下半空状态。在8.0中,因为默认开启innodb_stats_persistent,统计信息持久化也会占用一定的元数据页。如果业务侧有大量临时性的批量写入再删除,比如按月清理日志表,那么每个月都会留下一批不可用的空闲页,它们散布在文件各处,造成物理文件远大于select sum(data_length+index_length)计算出的逻辑大小。
可以通过information_schema或sys库观察碎片情况。下面的语句能估算每个表的碎片比例,帮助判断是否需要整理:
SELECT table_name, data_length, index_length, data_free, round(data_free / (data_length + index_length + 1) * 100, 2) AS frag_pct FROM information_schema.tables WHERE table_schema = 'test_db' AND engine = 'InnoDB' ORDER BY frag_pct DESC;
optimize table在8.0中的真实行为与限制
在MySQL 8.0中执行optimize table,服务器会先检查存储引擎,对InnoDB表实际上转化为ALTER TABLE ... ENGINE=INNODB的在线重建操作。这个过程会新建一个表空间文件,把旧表的有效数据按主键顺序重新写入,然后原子地替换掉旧表。因为数据是顺序落盘的,原来的空闲页不会被拷贝,所以新ibd文件通常会明显变小,物理空间得以整理。
不过这种整理并不是没有代价。在8.0早期版本,optimize table对普通表仍需要拿到元数据锁,并且在重建期间会产生大量redo与undo日志,对IO和缓冲池形成冲击。如果表很大,操作可能持续几十分钟甚至数小时。另外,尽管8.0支持 inplace 算法,但optimize的本质是重建,临时文件也会占用额外磁盘,必须确保剩余空间大于原表大小,否则会中途失败。
以下代码展示了在会话中安全执行整理并观察进度的一种方式:
-- 先确认表碎片比例较高 OPTIMIZE TABLE orders_2023; -- 如果担心锁表,可使用在线DDL显式指定算法 ALTER TABLE orders_2023 ENGINE=INNODB, ALGORITHM=INPLACE, LOCK=NONE;
需要注意的是,在具备写负载的从库上执行这类操作,还可能造成主从延迟。因此生产环境一般安排在维护窗口,或者采用pt-online-schema-change之类的外部工具来降低影响。
减少碎片与替代整理方案的实践建议
面对MySQL 8.0升级后的碎片问题,一味频繁执行optimize table并不可取。更合理的做法是先区分表类型。对于只插入不删除的流水表,碎片通常很低,不需要处理;对于高频更新删除的配置表或日志表,可以设定季度或半年一次的整理窗口。与此同时,合理设计主键为自增列,能让插入总是发生在索引末端,显著减少页分裂带来的碎片。
如果业务不能接受optimize的锁定时间,可以考虑使用MySQL 8.0的克隆插件或者搭建从库,在从库完成表重建后再切换流量。另外,将大表按时间分区,删除旧分区时使用ALTER TABLE ... DROP PARTITION,在8.0中该操作几乎瞬间完成且直接回收空间,比逐行delete再optimize要高效得多。分区表从设计上就规避了碎片堆积的难题。
下面给出一个分区维护的示例,通过直接丢弃分区来避免碎片:
-- 创建按天分区的日志表
CREATE TABLE sys_log (
id BIGINT AUTO_INCREMENT,
msg VARCHAR(255),
created_at DATETIME,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p20240101 VALUES LESS THAN (TO_DAYS('2024-01-02')),
PARTITION p20240102 VALUES LESS THAN (TO_DAYS('2024-01-03'))
);
-- 清理历史数据,直接丢弃分区,不产生碎片
ALTER TABLE sys_log DROP PARTITION p20240101;
综合来看,MySQL 8.0的表空间碎片更多是机制变更带来的可观测变化,而非缺陷。理解其原理后,用optimize table做周期性物理整理,或改用分区丢弃策略,都能让磁盘空间保持在合理水平。
MySQL_8.0表空间碎片optimize_table修改时间:2026-08-17 19:48:35