当我们的数据库存储了一些已经不需要的数据,我们需要清除节省空间之后,会发现,表文件的占用大小还是不变的,那么这是为什么呢?
那么今天就来围绕着数据库表的空间回收,看看如何处理这个问题。
首先,还是针对应用最广泛的InnoDB存储引擎进行分析这个问题,一个InnoDB分为两个部分,也就是定义表结构和数据。在Mysql8.0之前,表的结构数据存在.frm的文件里,而Mysql8.0版本开始,这些数据就放在了系统数据表里了,因为表本身的建表语句还是占用空间很少的。
接下来,首先说一下为什么删了一半表数据无法让这些数据进入表的空间回收,然后再说一下正确的空间回收的方法。
参数InnoDB_file_per_table
表数据既可以共享在表空间,也可以是单独的文件,这个行为是由参数InnoDB_file_per_table控制的:
- 这个参数是类似bool类型,如果是off,表的数据放在系统共享空间,也就是和数据字典放在一起。
- 这个参数设置的是on表示的是,每个InnoDB表数据存储在一个以.ibd后缀的文件。而从5.6开始,默认就是on。
但是不管是什么版本什么设置,最好都设置为on,因为如果不这么,把表数据放在共享表空间,drop table是不会触发空间回收,但是把表数据隔离出去,是可以触发空间回收的。
所以以下的讨论都是针对于参数设置为on展开的。
但是虽说上面是很直接的就drop table了,但是真的业务场景,我们都是去delete某一行的数据,问题是delete了之后,数据却没有因为消失,占有空间减小。
所以我们来看一下删除的流程。
数据删除流程
我们先再看一下InnoDB中索引的图,之前说过,InnoDB都是基于B+树去组织的。
假设我们要删除R4,InnoDB会把R4标记为删除,如果之后再插入一个ID在300-600的数据,就会复用这个位置,所以空间还是保留了,所以磁盘占用不变。
既然我们知道InnoDB是按照页去存储的,那么如果我们删掉了一个数据页上的所有记录,会怎么样?
答案是:整个数据也就可以被复用了。
但是一整页的复用和一个记录的复用是不同的。复用一个记录,限制了范围,复用一个页,可以拿过来任何种类的数据到任何位置。
但是如果相邻两个数据页的利用率小,就会在原来的位置上把两页进行合并,只不过那个被合并的另一个页面(是原来就有的页面,不是复用的页面)就会被标记为可以被复用。
我们进一步去猜想,如果用delete删除整个表的数据呢?结果就是会将所有的数据页都变为可以复用,但是在磁盘上文件占用不会减小。这也就形成了“空洞”
不止是删除数据会造成空洞,插入数据也会。如果数据是按照索引递增插入,那么就是紧凑的,但是如果是随机插入的,就会造成索引的数据页分裂。
假设一个数据页PageA已经满了,那么再插入一行数据会如何呢?
当满了之后,我们再插入数据,就肯定要重新申请一个新的页面来保存数据了,页分裂完成后,pageA的末尾就留下了空洞。
而要减少库表占用的磁盘,就需要收缩表,而重建表,就可以达到这样的目的。
重建表
如果我们新建一个和原来的表A相同结构的表B,那么按照主键ID自增的顺序,把数据一行行的从表A读出来再插入到B中,由于B是新建的表,所以不存在空洞,这样就起到了收缩表A的作用。
但是重建表也有不好的地方,比如我们在读取数据的时候有新的数据进入到了A,那么就无法将这个数据加载到B中了。所以这个操作要离线进行。
但是上面仅仅是mysql5.5之前的情况,在mysql5.6开始就有了新的优化:
- 建立一个临时文件,扫描A的所有数据页
- 根绝数据页生成B+树,存入临时文件
- 生成临时文件的过程中,将对A的操作存入一个日志文件,对应的是图中state2的状态
- 临时文件做好之后,根据日志文件的操作应用到临时文件,然后得到一个逻辑数据上和A相同的数据文件,对应state3
- 用临时文件替换表A的数据文件
和之前不同的地方就在于,这个方案在重建表的过程中允许对A进行crud,并且能复制到B表。
但是又不得不说,这个操作在操作大表上很浪费资源。