扒一扒InnoDB数据在硬盘上是如何存放的

1,096 阅读9分钟

前言

hello,小伙伴们下午好。俗话说的好,一时拖更一时爽,一直拖更一直爽。但是今天良心发现了,决定要更一下。高能预警,为了还债,特地写了篇长文。


索引组织表

在InnoDB存储引擎中,表都是按照主键顺序组织存放的,这种存储方式的表被称为索引组织表。

在InnoDB中,每张表都有各自的主键(Primary Key),如果在创建表的时候显式的定义主键,则InnoDB存储引擎会按如下方式选择或创建主键。

  • 首先判断表中是否有非空的索引,如果有则第一个定义的非空索引作为主键
  • 如果不符合上述条件,InnoDB存储引擎自动创建一个6个字节大小的指针
这样的描述太干瘪啦,我们来动手操作下。

1.选择第一个定义的非空索引

首先,我们创建表student,并填充两条测试数据,语句如下:

create table student(
a int ,
b int not null,
c int not null,
UNIQUE key (a),
UNIQUE key (c),
UNIQUE key (b)
);
insert into student select 1,2,3;
insert into student select 4,5,6;

运行结果如下,我们可以看出_rowid的值等于列c的值,那就说明当前存储的结构是将c作为主键的。另外a是可以为空的,虽然他定义唯一键的是第一个,但仍然不会作为主键。b虽然是先定义列,但是定义唯一键是在c之后,所以也不会被作为唯一键。


2.自动创建6个字节大小的指针

首先,我们创建表score,并填充两条测试数据,语句如下:

create table score(
a int ,
b int ,
c int,
UNIQUE key (a),
UNIQUE key (c),
UNIQUE key (b)
);
insert into score select 1,2,3;
insert into score select 4,5,6;

运行结果如下,直接报错了,不认为_rowid是他的列名,因为没有满足条件的列能成为他的主键,所以其就自动创建指针来解决问题啦。


InnoDB的逻辑存储结构(整体)

表空间

表空间可以看做是InnoDB存储引擎逻辑结构的最高层,所以的数据都存放在表空间里面。

在默认情况下,InnoDB存储引擎有一个共享表空间ibdata1,即所有数据都存放在这个表空间里面。当然也可以在my.ini参数文件中启用innodb_file_per_table,则每张表的数据就可以单独放在一个表空间中。

注意:即使启用innodb_file_per_table参数,一些回滚信息,系统事务信息等还是在共享的表空间中,其随着时间的变化大小还是会不断增大的。单独的表空间只会存放一些数据及索引信息。

在InnoDB存储引擎中,对段的管理都是由引擎自身所完成的,DBA不能也没必要对其进行控制。

区是由连续页组成的空间,在任何情况下每个区的大小都是1MB,页的大小为16kb,所以一个区一共有64个连续的页。

下面详细描述。

下面详细描述。

InnoDB行记录格式(重点)

InnoDB存储引擎和大多数数据库一样,记录是以行的形式存储的,这就意味着页中保存着表中的一行行数据。那么问题就来了,他这一行数据是包括哪些部分,除了具体的数据,还有其他的一些额外信息吗?

首先,我们先来看一下如果没有指定行格式,其默认的行格式是什么?

新建表test,拥有的字段a,b ,c 。

create table test(
a varchar(10),
b varchar (10) not null,
c char(10)
);
insert into test values( '001','Andy','compter');
insert into test values( '002','Bob',NULL);

如下图所示,默认的行格式是Compact ,该行格式是在MySQL5.0中引入的,其设计目标是高校的存储数据。简单来说,一个页存放的行数据越多,其性能越高。针对这个描述,咱先放在一边,之后看到其他的行格式,咱对比着看,为啥compact性能高?


下图为行格式Compact的大概结构,先瞅一眼,主要分为两个部分,额外信息和真实数据。额外信息包括变成字段长度,NULL值列表,记录头信息,真实数据即为该行记录有多少列,每列数据有哪些。其中记录头信息包括记录删除位,记录类型,下一个指针的位置。

注意,记录头信息还有很多为其他信息,但是重要的就这几个。是不是不知道他们是干撒的,一脸懵逼中,没事,这还是混个脸熟,下一部分来慢慢盘他。


变长字段长度

MySQL支持一些变长的数据类型,如varchar,text,blob,变长长度存储多少字节的数据是不固定的,所以我们在存储真实数据的时候,需要将这些数据占用的字节数也要存储起来。

刚才我们新增了两条数据,先拿第一个数据为例,将真正数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,逆序存放。如下图,所以最终第一条记录存放的十六进制为08 04 03,他们之间没有空格,是为了显示的效果才加了空格。那第二条记录很明显是03 03.

注意:如果表中没有变长字段,则该字段不存在。


NULL值列表

我们知道表中的某些列可能存储NULL值,如果这些NULL值放在记录的真实数据中存储会占用空间,所以Compact将这些值为NULL的列统一管理起来,存储在NULL表中。

注意:跟变长字段一样,如果表中没有NULL值的列,则该字段不存在。

注意:MySQL规定NULL值列表必须是整数个字节的位表示,如果使用的二进制位歌手不是整数个字节,则在字节的高位补0.

第一行数据虽然没有NULL值,但是a,c是可能存储NULL值的列,所以NULL值列表如下,0表示列所对应的值不为NULL,1表示列所对应的值为NULL。


第二行数据a不是NULL,c是NULL,所以对应的NULL值列表如下。


记录头信息

  • 记录删除位(delete_mask):0为未删除,1为删除
  • 记录类型(record_type):0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录。
  • 下一个指针的位置(next_record):表示从当前记录的真实数据到下一条记录的真实数据之间的地址偏移量。比如第一条记录的next_record为20,那么意味从第一条记录的真实数据的地址处向后找32个字节便是下一条记录的真实数据。实际上就是链表结构。

真实数据

InnoDB数据页结构(重点)

下图为数据页的整体结构,咱先了解个大概,再慢慢盘他。


文件头(File Header)

页的一些通用信息,包括该页属于哪个表空间,InnoDB存储引擎页的类型。

页头(Page Header)

记录数据页的状态信息。

最小记录+最大记录(Infimum+supermum)

在InnoDB存储引擎中,每个数据页都有两条虚拟的行记录,用来限定记录的边界。

Infimum记录是指比该页中任何主键值都要小的值,Supermum记录是指比该页中任何主键值都要大的值。

这两个值在页创建时都会被的创建,并且在任何情况下不会被删除。

这两条记录的构造十分简单,都是有5个字节大小的记录头信息和8个字节的固定部分组成的。

注意:上面提到了最小记录的record_type为2,最大记录的record_type为3。

用户记录(User Records)

重点来了,这边是数据实际存储位置,上面已经针对行格式做了详细的划分,现在咱就长话短说啦。

如果我删除了第二行记录,这条记录并不是立刻删除了,只是将删除记录位改为1啦。并且将他前面一条数据的指针指向他后面一条数据的地址,从而跳过这一条数据。

至于为什么会这样做呢?是为了节约时间和空间的消耗。如果在删除的时候,立刻从磁盘上移除,那么其他记录在磁盘上重新排列需要性能消耗,所以在删除的时候,只会将所有被删除的记录组成一个垃圾链表,稍后操作。或者有新纪录插入的时候,覆盖掉刚才的存储空间。




空闲空间(Free Space)

不重要。

页面目录(Page Directory)

我们现在已经找到记录在页面中按照主键由小到达顺序组成一个单链表,那如果想根据主键查询页中的某条记录怎么办?

最蠢的方法肯定是按单链表的顺序从头到尾的查找,因为只有知道前面一条记录的记录的地址,才能根据指针找到下一条记录。但是这个有个明显的缺点,就是太慢了,如果有1000条数据,一个个的查询,如果最后一条记录才满足条件,那就太浪费时间啦。

我们可以先从顺序表中想想,如果顺序表中要找一个记录,我们除了从头开始查之外,还可以采用二分法,可以提升查询速度。

那么在单链表中是否可以采用二分法呢?答案是肯定的。即采用目录的形式,将所有的记录划分为多个记录块,然后取每个记录块的最大的值,将其组成一个目录,在查找的时候,先查目录,能判断在哪个区间内。这个过程就类似于在书中找到某一个概念,要从目录先找一样。

文章尾部(File Tailer)

不重要。哈哈哈。

结束

码字不易,请多多关注哦。

写在最后

该篇借鉴的博文,书籍包括如下,特此感谢!

《MySQL技术内幕——InnoDB存储引擎》

MySQL是如何运行的

blog.csdn.net/u010922732/…

juejin.cn/post/684490…