存储结构

表空间

表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所有的数据都是存放在表空间中。默认情况下 InnoDB 存储引擎有一个共享表空间 ibdata1,即所有数据都放在这个表空间内。如果我们启用了参数 innodb_file_per_table,则每张表内的数据可以单独放到一个表空间内。

对于启用了 innodb_file_per_table 的参数选项,需要注意的是,每张表的表空间内存放的只是数据、索引和插入缓冲,其他类的数据,如撤销(Undo)信息、系统事务信息、二次写缓冲(double write buffer)等还是存放在原来的共享表空间内。这也就说明了另一个问题:即使在启用了参数 innodb_file_per_table 之后,共享表空间还是会不断地增加其大小。

表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。InnoDB 存储引擎表是索引组织的(index organized),因此数据即索引,索引即数据。那么数据段即为 B+树的页节点(上图的 leaf node segment),索引段即为 B+树的非索引节点(上图的 non-leaf node segment)。与 Oracle 不同的是,InnoDB 存储引擎对于段的管理是由引擎本身完成,这和 Oracle 的自动段空间管理(ASSM)类似,没有手动段空间管理(MSSM)的方式,这从一定程度上简化了 DBA 的管理。需要注意的是,并不是每个对象都有段。因此更准确地说,表空间是由分散的页和段组成。

区是由 64 个连续的页组成的,每个页大小为 16KB,即每个区的大小为 1MB。对于大的数据段,InnoDB 存储引擎最多每次可以申请 4 个区,以此来保证数据的顺序性能。在我们启用了参数 innodb_file_per_talbe 后,创建的表默认大小是 96KB;在每个段开始时,先有 32 个页大小的碎片页(fragment page)来存放数据,当这些页使用完之后才是 64 个连续页的申请。

create table t1 (
  col1 int not null auto_increment,
  col2 varchar (7000),
  primary key(col1)
)engine=InnoDB
system ls -lh /usr/local/var/mysql/test/t1.ibd

创建了 t1 表,col2 字段设为 varchar(7000),这样能保证一个页中可以存放 2 条记录。可以看到,初始创建完 t1 后表空间默认大小为 96KB。

同大多数数据库一样,InnoDB 有页(Page)的概念(也可以称为块),页是 InnoDB 磁盘管理的最小单位。与 Oracle 类似的是,Microsoft SQL Server 数据库默认每页大小为 8KB,不同于 InnoDB 页的大小(16KB),且不可以更改(也许通过更改源码可以)。常见的页类型有:数据页(B-tree Node),Undo 页(Undo Log Page),系统页(System Page),事务数据页(Transaction system Page),插入缓冲位图页(Insert Buffer Bitmap),插入缓冲空闲列表页(Insert Buffer Free List),未压缩的二进制大对象页(Uncompressed BLOB Page),压缩的二进制大对象页(Compressed BLOB Page)。

InnoDB 的数据页由以下 7 个部分组成:

  • 文件头(File Header) 固定 38 个字节 (页的位置,上一页下一页位置,checksum , LSN)

  • 数据页头( Page Header)固定 56 个字节 包含 slot 数目,可重用空间起始地址,第一个记录地址,记录数,最大事务 ID 等

  • 虚拟的最大最小记录 (Infimum + Supremum Record)

  • 用户记录 (User Records) 包含已经删除的记录以链表的形式构成可重用空间

  • 待分配空间 (Free spaces) 未分配的空间

  • 页目录 (Page Directory) slot 信息,下面单独介绍

  • 文件尾 (File Trailer) 固定 8 个字节,用来保证页的完整性

Slot

页目录里维护多个 slot ,一个 slot 包含多个行记录。每个 slot 占 2 个字节,记录这个 slot 里的行记录相对页初始位置的偏移量。由于索引只能定位到数据页,而定位到数据页内的行记录还需要在内存中进行二分查找,而这个二分查找就需要借助 slot 信息,先找到对应的 slot ,然后在 slot 内部通过数据行中记录头里的下一个记录地址进行遍历。每一个 slot 可以包含 4 到 8 个数据行。如果没有 slot 辅助,链表本身是无法进行二分查找的。

InnoDB 存储引擎是面向行的(row-oriented),也就是说数据的存放按行进行存放。每个页存放的行记录也是有硬性定义的,最多允许存放 16KB/2~200 行的记录,即 7992 行记录。这里提到面向行(row-oriented)的数据库,那么也就是说,还存在有面向列(column-orientied)的数据库。MySQL infobright 储存引擎就是按列来存放数据的,这对于数据仓库下的分析类 SQL 语句的执行以及数据压缩很有好处。类似的数据库还有 Sybase IQ、Google Big Table。面向列的数据库是当前数据库发展的一个方向。

一行记录最终以二进制的方式存储在文件里,我们要能够解析出一行记录中每个列的值,存储的时候就需要有固定的格式,至少需要知道每个列占多少空间,而 MySQL 中定义了一些固定长度的数据类型,例如 int、tinyint、bigint、char 数组、float、double、date、datetime、timestamp 等,这些字段我们只需要读取对应长度的字节,然后根据类型进行解析即可,对于变长字段,例如 varchar、varbinary 等,需要有一个位置来单独存储字段实际用到的长度,当然还需要头信息来存储元数据,例如记录类型,下一条记录的位置等。

行结构

  • 变长字段长度列表,该位置用来存储所申明的变长字段中非空字段实际占有的长度列表,例如有 3 个非空字段,其中第一个字段长度为 3,第二个字段为空,第三个字段长度为 1,则将用 01 03 表示,为空字段将在下一个位置进行标记。变长字段长度不能超过 2 个字节,所以 varchar 的长度最大为 65535。

  • NULL 标志位,占 1 个字节,如果对应的列为空则在对应的位上置为 1 ,否则为 0 ,由于该标志位占一个字节,所以列的数量不能超过 255。如果某字段为空,在后面具体的列数据中将不会在记录。这种方式也导致了在处理索引字段为空的时候需要进行额外的操作。

  • 记录头信息,固定占 5 字节,包含下一条记录的位置,该行记录总长度,记录类型,是否被删除,对应的 slot 信息等

  • 列数据 包含具体的列对应的值,加上两个隐藏列,事务 ID 列和回滚指针列。如果没有申明主键,还会增加一列记录内部 ID。

CREATE TABLE mytest(
t1 varchar(10),
t2 varchar(10),
t3 char(10),
t4 varchar(10)
) engine = innodb;
insert into mytest VALUES('a','bb','bb','ccc');
insert into mytest VALUES('d',NULL,NULL,'fff');

该表定义了 3 个变长字段和 1 个定长字段,然后插入两行记录,第二行记录包含空值,我们打开表空间 mytest.ibd 文件,转换为 16 进制,并定位到如下内容:

//第一行记录
03 02 01 为变长字段长度列表,这里是倒序存放的,分别对应 ccc、bb、a 的长度。
00 表示没有为空的字段
00 00 10 00 2c 为记录头
00 00 00 2b 68 00 没有申明主键,维护内部 ID
00 00 00 00 06 05 事务ID
80 00 00 00 32 01 10 回滚指针
61 第一列 a 的值
62 62 第二列 bb 的值
62 62 20 20 20 20 20 20 20 20 第三列 bb 的值,固定长度 char(10) 以20进行填充
63 63 63 第四列 ccc 的值
//第二行记录
03 01 为变长字段长度列表,这里是倒序存放的,分别对应 fff、a 的长度,第二列位空。
06 转换为二进制为 00000110 表示第二列和第三列为空
00 00 20 ff 98 为记录头
00 00 00 2b 68 01 没有申明主键,维护内部 ID
00 00 00 00 06 06 事务ID
80 00 00 00 32 01 10 回滚指针
64 第一列 d 的值
65 65 65 第四列 fff 的值