以一个问题开始本章的学习:为什么删除了表的数据,表文件的大小还是没有变?

Innodb的一个表包含两部分:表结构定义和表数据,8.0之前表结构是存放在.fm后缀文件中的,8.0开始已经允许把表结构放在系统数据表中了。因为表结构定义占用的空间很小,所以本章讨论的问题主要是表数据。

先回答开始的问题,为什么简单地删除表数据达不到表空间回收的效果?

innodb_file_per_table参数

这个参数控制表数据是存放在共享表空间里还是单独的文件的。

  1. 参数为OFF表示,表的数据放在系统共享表空间,也就是跟数据字典放在一起。
  2. 参数为ON表示,每个InnoDB表数据存储在一个以.ibd为后缀的文件中。

从5.6.6版本开始,这个默认值就是ON了,也就是默认将表数据存储在.ibd文件中。

将innodb_file_per_table参数设置为ON更加推荐,将一个表的数据存为单独的文件更加容易管理,在drop table命令删除表的时候,系统就会直接删除这个文件。如果放在共享表空间中,即使表删掉了,空间也不会回收的

下面的所有讨论也都是基于innodb_file_per_table=ON展开的。

在删除整个表的时候,可以使用drop table命令回收表空间。但是,我们遇到的更多场景是删除某些行,本章开始的问题就是在这种场景下出现的,表的数据被删除了,但是表空间没有回收。

数据删除流程

mysql里的数据都是用B+树的结构组织的。 这里我们要删除R4这个记录,Innodb只会把R4这个记录标记为删除,并没有真正进行删除,所以磁盘文件的大小并不会改变,并且后续如果插入在300~600之家的记录,可能还会复用这个空间。

如果删除了整个数据页的话,那么就会把整个数据页变为可复用的,并且不像记录复用需要满足条件约束,数据页可以复用到任何位置。当相邻两个数据页利用率都很小,系统就会把这两个页的数据合到其中一个页上,另外一个数据页就会被标记为可复用。

所以,使用delete删除整个表的数据,就是所有的数据页都变成了可复用的,但是磁盘上的文件大小并不会变小,即使用delete不能回收表空间

这些可复用但是没有使用的空间,看起来就像是“空洞”。并且如果插入记录的时候不是按照索引递增的顺序插入的话,就可能造成索引的数据页分裂,也会产生空洞。

另外,更新索引上的值,可以理解为删除一个旧的值,再插入一个新的值。也是会造成空洞的。所以经过大量增删改的表,都是可能存在空洞的。将这些空洞去掉就能够达到收缩表空间的目的,重建表就能够达到这样的目的

重建表

现在要将表A进行空间收缩,对其进行重建。

先创建一个与表A结构相同的表B,然后将A中的数据按照主键ID递增的顺序一行一行从A中读取出来再插入到B中。这样B中就不存在A中的空洞了,然后用表B替换表A,这样就成功收缩了表A。

使用alter table A engine=InnoDB命令可以完成这一点,再5.5版本之前,这个命令的执行流程与前面的描述差不多,只是这个临时表B不需要我们创建,mysql会自动完成转存数据,交换表名,删除旧表的操作。

在这个重建表结构的过程中,是不能有新数据要写入到表A的,否则就会造成数据丢失,也就是说,这个DDL不是Onlne的。

MySQL5.6版本开始引入了Online DDL,对这个流程做了优化。这个新的流程为:

  1. 建立一个临时文件,扫描表A主键的所有数据页;
  2. 用数据页中表A的记录生成B+树,存储到临时文件中;
  3. 生成临时文件的过程中,将所有对A的操作记录在一个日志文件中,对应图中state2的状态。
  4. 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表A相同的数据文件,对应图中state3的状态;
  5. 用临时文件替换表A的数据文件。

其实很好理解,就是建立临时表的时候将对原表的更新操作存放到日志当中,当临时表建立号之后,再把日志重放就可以保证数据不丢失了。允许在重建表的时候,对表A做增删改操作,这样就是Online DDL名字的来源。

不过我们在5.锁中讲到过,DDL之前是要拿到MDL写锁的,而增删改是要拿到MDL读锁的,增么能够Online DDL呢?alter语句在启动的时候需要获取MDL写锁,但是这个写锁在真正拷贝数据之前就退化为读锁了。这样MDL读锁就不会阻塞增删改的操作,而之所以还要保留读锁,当然是为了避免其他线程同时对这个表做DDL。

对于一个大表来说,重建表的时候需要扫描原表数据并拷贝数据到临时文件,这个操作很消耗IO和CPU资源,所以线上服务,要很小心地控制操作时间,推荐使用Github开源的gh-ost来做。

Online和inplace

可以看到在上面的Online DDL重建表的过程中,表A中的数据导出来存放的位置叫做tmp_file,这个临时文件是InnoDB在内部创建出来的,整个DDL过程都在InnoDB内部完成,对于server层来说,没有把数据挪动到临时表,是一个“原地”操作,这就是inplace名称的来源

所以inplace就是说,没有将数据拷贝到server层,但是在innodb引擎层,tmp_file就是一个拷贝的文件,也要占用临时空间。

alter table t engine=InnoDB,隐含意思是:

alter table t engine=innodb,ALGORITHM=inplace;

跟inplace对应的就是拷贝表的方式:

alter table t engine=innodb,ALGORITHM=copy;

要注意,这里说的拷贝是指将数据拷贝到了server层。

Online是Inplace的子集,DDL过程是Online的,就一定是inplace,反之就不成立。例如mysql8.0之前添加全文索引和空间索引就不是online的。

小结

通过这里就可以明白了,如果要收缩一个表,通过delete掉里面不用的数据,表文件的大小是不会改变的。需要通过alter命令重建表才能够收缩表文件的大小。

  • alter table t engine=InnoDB是recreate
  • analyze table t不是重建表,只是对表的索引信息做重新统计,这个过程中加了MDL读锁。
  • optimize table t等于recreate + analyze。