文章目录
- 表
- 索引组织表
- InnoDB逻辑存储结构
- 表空间(tablespace)
- 段(segment)
- 区(extent)
- 页(page)
- 行(record)
- 行记录格式
- compact
- Redundant
- 行溢出数据
- Compressed 和 Dynamic 行记录格式
- CHAR的行结构存储
- 数据页结构
- File Header & Page Header & File Trailer
- Infimum 和 Supremum Record
- User Record 和 Free Space
- Page Directory
- InnoDB数据页结构示例分析
- Named File Formats 机制
表
索引组织表
在InnoDB存储引擎中,表都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table)。在 InnoDB存储引擎表中,每张表都有个主键(Primary Key),如果在创建表时没有显式地定义主键,则InnoDB存储引擎会按如下方式选择或创建主键∶
- 首先判断表中是否有非空的唯一索引(Unique NOT NULL),如果有,则该列即为主键,如果有多个非空唯一索引时,InnoDB存储引擎将选择建表时第一个定义的非空唯一索引为主键(主键的选择根据的是定义索引的顺序,而不是建表时列的顺序);
- 如果不符合上述条件,InnoDB存储引擎自动创建一个6字节大小的指针_rowid来表示主键。
如下所示:
create table test_rowid(
a int not null,
b int not null,
c int,
unique key(b), unique key(a)
)engine=innodb;
insert into test_rowid_2(a,b) values(1,2);
select *,_rowid from test_rowid_2;
但是有时候查看_rowid会报错:
ERROR 1054 (42S22): Unknown column '_rowid' in 'field list'
这是因为_rowid根据情况是会隐藏的,可以确定的是,只要创建表,这个_rowid一定是存在的,唯一区别就是显示和隐示的区别,也就是是否可以通过select _rowid from table查询出来,以下情况时显示的:
- 当表中有主键并且是数值型的时候才是显示的;
- 当表中没有主键的时候,但是表中有唯一非空并且是数值型的时候才是显示的。
并且_rowid 只能用于查看单个列为主键的情况,对于多列组成的主键就显得无能为力了。
InnoDB逻辑存储结构
从 InnoDB存储引擎的逻辑存储结构看,所有数据都被逻辑地存放在一个空间中,称之为表空间(tablespace)。表空间又由段(segment)、区(extent)、页(page)组成。页在一些文档中有时也称为块(block),InnoDB存储引擎的逻辑存储结构大致如图所示。
表空间(tablespace)
MySQL中最顶层的逻辑管理结构是表空间,所有的数据都存放在表空间中。在默认情况下 InnoDB存储引擎有一个共享表空间 ibdata1,即所有数据都存放在这个表空间内。如果用户启用了参数 innodb_file_per_table,则每张表内的数据可以单独放到一个表空间内。
如果启用了innodb_file_per_table的参数,需要注意的是每张表的表空间内存放的只是数据、索引和插入缓冲Bitmap 页,其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在共享表空间中。
根据用途表空间分为如下几类:
- 系统表空间:存放数据字典(data dict)、双写缓存(double write buffer)、变更缓存(change buffer)、回滚日志(undo log)及可能的表数据和索引。文件信息由innodb_data_home_dir和innodb_data_file_path控制,默认情况下有一个ibdata1文件。
- 独占表空间:存放表数据和索引,是否开启由innodb_file_per_table控制,默认开启,开启后每个表会生成独立的ibd文件。
- 通用表空间:通过create tablespace语法创建的共享表空间。
- Undo表空间:存放undo log,文件路径由innodb_undo_directory控制,表空间个数由innodb_undo_tablespaces控制,undo log默认存储在系统表空间中。
- 临时表空间:存放临时表,文件路径由innodb_temp_data_file_path控制。
当在一个表中执行多次update语句,共享表空间ibdata的大小会增加,但InnoDB存储引擎不会在执行rollback 时去收缩这个表空间,也就是ibdata的大小不变。虽然 InnoDB不会回收这些ibdata空间,但是会自动判断这些 undo信息是否还需要,如果不需要,则会将这些空间标记为可用空间,供下次 undo 使用。
回想一下,在前面提到的 master thread每10秒会执行一次的 full purge操作,很有可能的一种情况是:用户再次执行上述的 UPDATE 语句后,会发现 ibdata1不会再增大了。
段(segment)
空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。InnoDB存储引擎表是索引组织的(index organized),因此数据即索引,索引即数据。那么数据段即为B+树的叶子节点(Leaf node segment),索引段即为B+树的非索引节点(Non-leaf node segment)。
为节省空间,每个segment都先从FSP HEADER的FSP_FREE_FRAG中分配32个碎片页(FSEG_FRAG_ARR),当这些32个页面不够使用时,再申请区。
每个INODE PAGE默认可存储85个SEGMENT INODE。每个索引使用2个segment,分别用于管理叶子节点和非叶子节点。对于用户表,其索引的Root Page中保存了两个SEGMENT HEADER,分别指向叶子节点的SEGMENT INODE和非叶子节点的SEGMENT INODE。
所以一个INODE PAGE最多可以保存42个索引信息(一个索引使用两个段)。如果表空间有超过42个索引,则必须再分配一个INODE PAGE。
在 InnoDB存储引擎中,对段的管理都是由引擎自身所完成。
区(extent)
区是由连续页组成的空间,在任何情况下每个区的大小都为1MB。为了保证区中页的连续性,InnoDB存储引擎一次从磁盘申请4~5个区。在默认情况下,InnoDB存储引擎页的大小为16KB,即一个区中一共有64个连续的页。
- InnoDB 1.0.x版本开始引入压缩页,即每个页的大小可以通过参数KEYBLOCK SIZE设置为2K、4K、8K,因此每个区对应页的数量就应该为512、256、128。
- InnoDB 1.2.x版本新增了参数 innodb_page _size,通过该参数可以将默认页的大小设置为 4K、8K,但是页中的数据库不是压缩。这时区中页的数量同样也为256、128。总之,不论页的大小怎么变化,区的大小总是为1M。
在用户启用了参数innodb_file_per_talbe 后,创建的表默认大小是96KB。区中是64个连续的页,创建的表的大小至少是1MB才对啊?其实这是因为在每个段开始时,先用32个页大小的碎片页(fragment page)来存放数据,在使用完这些页之后才是64个连续页的申请。这样做的目的是:对于一些小表,或者是 undo 这类的段,可以在开始时申请较少的空间,节省磁盘容量的开销。这里可以通过一个很小的示例来显示InnoDB存储引擎对于区的申请方式∶
(1)第一步:创建一个t1表,将col2字段设为VARCHAR(7000),这样就能保证一个页最多可以存放2条记录(因为一个页的大小为16KB*1024=16384bit)。然后通过ls命令查看到表空间默认大小为96KB:
(2)第二步:接着向表中插入两条SQL语句,然后查看表空间的大小发现空间没有增加,并且这两条记录应该位于同一个页中:
此时再去使用py_innodb_page_info工具查看表空间,可以看到:
- page offset为3的页:这个就是数据页
- page level:表示所在索引层,0表示叶子节点。因为当前所有记录都在一个页中,因此没有非叶子节点
(3)第三步:现在我们再插入一条记录,就会产生一个非叶节点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eO8cRKBM-1671369273688)(null)]
page offset为3的页的page level由之前的0变为了1,这时虽然新插入的记录导致B+树的分裂操作,但这个页的类型还是B-tree Node
(4)第四步:接着上述同样的操作,再插入60条记录,也就是说当前表t1中共有63条记录,32个页。为了导入的方便,在这之前先建立一个导入的存储过程
可以看到,在导入了63条数据后,表空间的大小还是小于1MB,即表示数据空间的申请还是通过碎片页,而不是通过64个连续页的区:
再次查看表空间文件,可以观察到B-tree Node一共有33个,除去一个page level为1的非叶节点页,一共有32个page level为0的页,也就是说,对于数据库,已经有32个碎片页了。之后用户再申请空间,则表空间按连续64个页的大小开始增长了:
(5)第五步:现在继续插入一条记录,之后看看表空间的大小。因为已经用完了32个碎片页,新的页回采用区的方式进行空间的申请:
现在再次分析表空间文件:
页(page)
同大多数数据库一样,InnoDB有页(Page)的概念(也可以称为块),页是InnoDB 磁盘管理的最小单位。在InnoDB存储引擎中,默认每个页的大小为16KB。而从InnoDB 1.2.x版本开始,可以通过参数innodb_page_size 将页的大小设置为4K、8K、16K。若设置完成,则所有表中页的大小都为innodb_page_size,不可以对其再次进行修改。除非通过 mysqldump 导入和导出操作来产生新的库。
在 InnoDB存储引擎中,常见的页类型有∶
- 数据页(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)
行(record)
InnoDB存储引擎是面向列的(row-oriented),也就说数据是按行进行存放的。每个页存放的行记录也是有硬性定义的,最多允许存放16KB/2-200行的记录,即 7992行记录。
行记录格式
Innodb行格式有四种:redundant、compact、compressed、dynamic,参考源码:rem0types.h/rec_format_enum。其中redundant为旧格式,compact、compressed、dynamic为新格式,新旧格式在记录存储格式上差异较大。接来下会详细介绍不同格式下记录的存储方式。
Compressed和Dynamic是Compact的变种形式。他们基本没什么本质上的区别,唯一的区别就是对于行溢出的处理不同。Compressed在数据页只存储一个指向溢出页的地址,所有的实际数据都存放在溢出页中。
而Compressed还可以是zlib算法对行数据进行压缩,因此对于BLOB,TEXT,VARCHAR这类大长度类型的数据能够非常有效的存储。
compact
Compact 行记录是在 MySQL5.0中引入的,其设计目标是高效地存储数据。简单来说,一个页中存放的行数据越多,其性能就越高。图显示了Compact 行记录的存储方式∶
-
Compact 行记录格式的首部是一个非 NULL 变长字段长度列表,并且其是按照列的顺序逆序放置的,其长度为∶
(1)若列的长度小于255字节,用1字节表示每列;
(2)若大于255个字节,用2字节表示没列。
变长字段的长度最大不可以超过2字节,这是因在 MySQL数据库中VARCHAR类型的最大长度限制为65535字节。
这里特别要注意的是:从Mysql4.1版本开始,CHAR(N)指的是字符长度,而不是字节长度,也就是说在不同的字符集下,CHAR类型内部存储的可能不是定长的数据,比如:在latin1编码下,都是单字节;在gbk编码下,英文是单字节,中文是两字节;在utf-8编码下,英文是单字节,中文是三字节。也就是说在utf-8下,CHAR(10)最小可以存储10字节的字符,最大可以存储30字节的字符。因此对于多字节字符编码的CHAR数据类型的存储,InnoDB存储引擎在内部将其视为变长字符类型。
-
变长字段之后的第二个部分是NULL标志位,该位指示了该行数据中是否有NULL值,有则用1表示(也就是每位表示一个列,列位NULL,则对应的位为1)
接下来的部分是记录头信息(record header),固定占用5字节(40 位):
最后的部分就是实际存储每个列的数据。需要特别注意的是:
- NULL 不占该部分任何空间,即 NULL除了占有 NULL标志位,实际存储不占有任何空间。
- 每行数据除了用户定义的列外,还有两个隐藏列,事务 ID列和回滚指针列,分别为6字节和7字节的大小。若InnoDB表没有定义主键,每行还会增加一个6字节的 rowid列。
实际数据根据索引类型存储方式不一样,分为:聚簇索引非叶子节点、聚簇索引叶子节点、二级索引非叶子节点、二级索引叶子节点。格式如下:
-
聚簇索引非叶子节点:
-
聚簇索引叶子节点:
-
二级索引非叶子节点:
-
二级索引叶子节点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vJSdsMId-1671369271423)(http://image.codekiller.top/img/mysql/mysql5_37.png)]
接下去用一个具体示例来分析 Compact 行记录的内部结构∶
在上述示例中,创建表mytest,该表共有4个列。t1、t2、t4都为VARCHAR变长字段类型,t3为固定长度类型 CHAR(因为是latin1编码)。接着插入了3条有代表性的数据,然后将打开表空间文件mytest.ibd(这里启用了innodb_file_per_table,若没有启用该选项,打开默认的共享表空间文ibdatal)。
- 现在第一行数据就展现在用户眼前了。需要注意的是,变长字段长度列表是逆序存放的,因此变长字段长度列表为03 02 01,而不是01 02 03。此外还需要注意 InnoDB每行有隐藏列TransactionID和 Roll Pointer。同时可以发现,固定长度 CHAR字段在未能完全占用其长度空间时,会用0x20来进行填充。
- 接着再来分析下 Record Header 的最后两个字节,这两个字节代表 next recorder,0x2c代表下一个记录的偏移量,即当前记录的位置加上偏移量 0x2c 就是下条记录的起始位置。所以InnoDB 存储引擎在页内部是通过一种链表的结构来串连各个行记录的。
现在来关心有NULL值的第三行数据:
第三行有NULL值,因此NULL标志位不再是00而是06,转换成二进制为00000110,为1的值代表第2列和第3列的数据为NULL。在其后存储列数据的部分,用户会发现没有存储NULL列,而只存储了第1列和第4列非NULL的值。
因此这个例子很好地说明了:不管是CHAR类型还是VARCHAR类型,在compact 格式下NULL 值都不占用任何存储空间。
Redundant
Redundant是MySQL5.0版本之前InnoDB的行记录存储方式,MySQL5.0支持Redundant 是为了兼容之前版本的页格式。Redundant行记录采用如下方式存储:
-
Redundant行记录格式的首部是一个字段长度偏移列表,同样是按照列的顺序逆序放置的。
如果字段为null,最高位为1,如果偏移量使用2字节,并且字段值使用外部存储,则次高位为1。
注意:CHAR类型的NULL值需要占用空间,全为0。
-
第二个部分为记录头信息(record header),不同于Compact行记录格式,Redundant 行记录格式的记录头占用6字节(48 位),每位的含义如下:
可以发现,n_fields 值代表一行中列的数量,占用10位。同时这也很好地解释了为什么 MySOL 数据库一行支持最多的列为1023。另一个需要注意的值为1byte_offs_flags,该值定义了偏移列表占用1字节还是2字节。而最后的部分就是实际存储的每个列的数据了。
实际数据根据索引类型存储方式不一样,分为:聚簇索引非叶子节点、聚簇索引叶子节点、二级索引非叶子节点、二级索引叶子节点。格式如下:
-
聚簇索引非叶子节点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YpCqoME2-1671369272992)(null)]
-
聚簇索引叶子节点:
-
二级索引非叶子节点:
-
二级索引叶子节点:
接着创建一张和表mytest内容完全一样但行格式为redundant的表mytest2:
可以看到,现在row_ format 变为Redundant。同样通过hexdump将表空问mytest2.ibd导出到文本文件mytest2.txt。打开文件,找到类似如下行:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WhbtiQBg-1671369273719)(null)]
23 20 16 14 13 0c 06逆转为06,0c,13,14,16,20,23,分别代表第一列长度6,第二列长度6 (6+6=0x0C),第三列长度为7 (6+6+7=0x13),第四列长度1 (6+6+7+1=0x14), 第五列长度2 (6+6+7+1+2 =0x16),第六列长度10 (6+6+7+1+2+10=0x20), 第七列长度3(6+6+7+1+2+10+3=0x23)。
在接下来的记录头信息(Record Header)中应该注意48位中的第22~ 32位,为00000111,表示表共有7个列(包含了隐藏的3列),接下来的第33位为1,代表偏
移列表为一个字节。
后面的信息就是实际每行存放的数据了,这同Redundant行记录格式大致相同,注意是大致相同,因为如果分析第三行,会发现对于NULL值的处理两者是非常不同的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U4QJa6qR-1671369273641)(null)]
这里与之前Compact行记录格式有着很大的不同了,首先来看长度偏移列表,逆序排列后得到06 0c 13 14 94 9e 21,前4个值都很好理解,第5个NULL值变为了94,接着第6个CHAR类型的NULL值为9e (94+10=0x9e),之后的21代表(14+3=0x21)可以看到对于VARCHAR类型的NULL值,Redundant 行记录格式同样不占用任何存储空间,而CHAR类型的NULL值需要占用空间。
当前表mytest2的字符集为Latin1,每个字符最多只占用1字节。若用户将表mytest2的字符集转换为utf8,第三列CHAR固定长度类型不再是只占用10字节了,而
是10X3= 30字节。所以在Redundant行记录格式下,CHAR类型将会占用可能存放的最大值字节数。
行溢出数据
InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外。一般认为 BLOB、LOB 这类的大对象列类型的存储会把数据存放在数据页面之外。但是,这个理解有点偏差,BLOB 可以不将数据放在溢出页面,而且即便是VARCHAR列数据类型,依然有可能被存放为行溢出数据,因为数据页只保存前768字节的前缀数据,之后是偏移量,指向溢出页,也就是Uncompressed BLOB Page。
首先对VARCHAR数据类型进行研究。MySQL数据库的VARCHAR类型可以存放65535字节。但是,这是真的吗?真的可以存放65535字节吗?如果创建VARCHAR长度为65535的表,用户会得到下面的错误信息∶
从错误消息可以看到InnoDB存储引擎并不支持65535长度的VARCHAR。这是因为还有别的开销,通过实际测试发现能存放 VARCHAR类型的最大长度为65532。
还需要注意上述创建的 VARCHAR长度为65532的表,其字符类型是 latin1 的,如果换成 GBK 又或 UTF-8的,会产生怎样的结果呢?
-
VARCHAR(N)中的N指的是字符的长度。而上面说明VARCHAR类型最大支持65535,单位是字节。
-
此外需要注意的是,MySQL官方手册中定义的 65535 长度是指所有VARCHAR列的长度总和,如果列的长度总和超出这个长度,依然无法创建。
3个列长度总和是66000,因此 InnoDB存储引擎再次报了同样的错误。即使能存放65532个字节,但是有没有想过,InnoDB存储引擎的页为16KB,即16384字节,怎么能存放65532字节呢?因此,在一般情况下,InnoDB存储引擎的数据都是存放在页类型为B-tree node 中。但是当发生行溢出时,数据存放在页类型为 Uncompress BLOB页中,数据页只保存前768字节的前缀数据,之后是偏移量,指向溢出页,也就是Uncompressed BLOB Page。
那多长的VARCHAR是保存在单个数据页中的,从多长开始又会保存在BLOB顶呢?
InnoDB存储引擎表是索引组织的,即 B+Tree 的结构,这样每个页中至少应该有两条行记录(否则失去了B+Tree 的意义,变成链表了)。因此,如果页中只能存放下一条记录,那么 InnoDB存储引擎会自动将行数据存放到溢出页中。
如果在一个页至少放入两行数据,那Varchar类型的行数据不会放到BLOB页中去,这个阈值长度为8098字节。
另一个问题是,对于TEXT或BLOB的数据类型,用户总是以为它们是存放在Uncompressed BLOB Page 中的,其实这也是不准确的。是放在数据页中还是 BLOB页中,和前面讨论的VARCHAR一样,至少保证一个页能存放两条记录。如:建立含有BLOB类型列的表,然后插人4行数据长度为8000的记录,发现其实数据并没有保存到BLOB页中:
当然既然用户使用了BLOB列类型,一般不可能存放长度这么小的数据。因此在大多数的情况下BLOB 的行数据还是会发生行溢出,实际数据保存在 BLOB 页中,数据页只保存数据的前768字节。
Compressed 和 Dynamic 行记录格式
InnoDB 1.0.x 版本开始引入了新的文件格式(file format,用户可以理解为新的页格式),以前支持的 Compact 和Redundant 格式称为 Antelope 文件格式,新的文件格式称为 Barracuda 文件格式。
Barracuda 文件格式下拥有两种新的行记录格式:Compressed和 Dynamic。
主要有两种不同:
-
新的两种记录格式对于存放在 BLOB中的数据采用了完全的行溢出的方式,在数据页中只存放 20个字节的指针,实际的数据都存放在 Off Page 中,而之前的 Compact 和 Redundant 两种格式会存放 768个前缀字节。
-
Compressed 行记录格式的另一个功能就是,存储在其中的行数据会以 zlib 的算法进行压缩,因此对于 BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。
CHAR的行结构存储
通常理解VARCHAR 是存储变长长度的字符类型,CHAR 是存储固定长度的字符类型。而在前面的小节中,用户已经了解行结构的内部的存储,并可以发现每行的变长字段长度的列表都没有存储 CHAR类型的长度。
从MySQL 4.1版本开始,CHAR(N)中的N指的是字符的长度,而不是之前版本的字节长度。也就说在不同的字符集下,CHAR类型列内部存储的可能不是定长的数据。例如下面的这个示例∶
往 gbk编码的 char(2)中,插入了我们ab两个数据,查看所占字节:
前两个记录’ab和’我们’字符串长度都为2,但是在内部存储时’ab’占2字节,'我们’占4个字节。
也就是说在不同的字符集下,CHAR类型内部存储的可能不是定长的数据,比如:在latin1编码下,都是单字节;在gbk编码下,英文是单字节,中文是两字节;在utf-8编码下,英文是单字节,中文是三字节。也就是说在utf-8下,CHAR(10)最小可以存储10字节的字符,最大可以存储30字节的字符。因此对于多字节字符编码的CHAR数据类型的存储,InnoDB存储引擎在内部将其视为变长字符类型。
上述例子清楚地显示了InnoDB存储引擎内部对CHAR类型在多字节字符集类型的存储。CHAR类型被明确视为了变长字符类型,对于未能占满长度的字符还是填充 0x20。因此可以认为在多字节字符集的情况下,CHAR和 VARCHAR的实际行存储基本是没有区别的。
数据页结构
页是 InnoDB存储引擎管理数据库的最小磁盘单位。页类型为B-tree Node(FIL_PAGE_INDEX) 的页存放的即是表中行的实际数据了。Index page用于存储数据和索引。Index page总体结构如下图所示:
InnoDB数据页由以下 7个部分组成:
其中File Header、Page Header、File Trailer 的大小是固定的,分别为38、56、8字节,这些空间用来标记该页的一些信息,如 Checksum,数据页所在 B+树索引的层数等。User Records、Free Space、Page Directory 这些部分为实际的行记录存储空间,因此大小是动态的。
File Header & Page Header & File Trailer
File Header用来记录页的一些头信息,共占用38字节。
接着 File Header部分的是Page Header,该部分用来记录数据页的状态信息,由14个部分组成,共占用56字节。
每个page具有相同的尾部(File Trailer),该尾部占用固定8字节大小,是为了检测页是否已经完整地写入磁盘(如可能发生的写入过程中磁盘损坏、机器关机等)。
具体看后面的ibd文件格式解析。
Infimum 和 Supremum Record
在InnoDB存储引擎中,每个数据页中有两个虚拟的行记录(伪行记录),用来限定记录的边界。
- Infimum 记录是比该页中任何主键值都要小的值;
- Supremum指比任何可能大的值还要大的值。
这两个值在页创建时被建立,并且在任何情况下不会被删除。在 Compact 行格式和 Redundant行格式下,两者占用的字节数各不相同。
(1)compact & compressed & dynamic 格式的记录:
- Infimum记录存储格式:
- Supremum记录存储格式:
(2)redundant 格式的记录:
-
Infimum记录存储格式:
-
Supremum记录存储格式:
User Record 和 Free Space
User Record就是之前讨论过的部分,即实际存储行记录的内容。再次强调,InnoDB 存储引擎表总是 B+ 树索引组织的。
Free Space很明显指的就是空闲空间,同样也是个链表数据结构。在一条记录被删除后,该空间会被加入到空闲链表中。
Page Directory
Page Directory(页目录)中存放了记录的相对位置(注意,这里存放的是页相对位置,而不是偏移量),有些时候这些记录指针称为 Slots(槽)或目录槽(Directory Slots)。
与其他数据库系统不同的是,在 InnoDB中并不是每个记录拥有一个槽,InnoDB 存储引擎的槽是一个稀疏目录(sparse directory),即一个槽中可能包含多个记录。伪记录Infimum 的n_owned值总是为1,记录 Supremum 的n_owned 的取值范围为【1,8】,其他用户记录n_owned 的取值范围为【4,8】。当记录被插入或删除时需要对槽进行分裂或平衡的维护操作。
在 Slots 中记录按照索引键值顺序存放,这样可以利用二叉查找迅速找到记录的指针。假设有(‘i’,‘d’,‘c’,‘b’,‘e’,‘g’,‘1’,‘h’,‘f’,'j,‘k’,‘a’),同时假设一个槽中包含4条记录,则 Slots 中的记录可能是(‘a’,‘e’,‘i’)。
由于在 InnoDB存储引擎中Page Direcotry 是稀疏目录,二叉查找的结果只是一个粗略的结果,因此 InnoDB存储引擎必须通过 recorder header 中的 next_record来继续查找相关记录。同时,Page Directory很好地解释了recorder header中的n_owned值的含义,因为这些记录并不包括在 Page Directory 中。
需要牢记的是,B+ 树索引本身并不能找到具体的一条记录,能找到只是该记录所在的页。数据库把页载入到内存,然后通过Page Directory 再进行二叉查找。只不过二叉查找的时间复杂度很低,同时在内存中的查找很快,因此通常忽略这部分查找所用的时间。
InnoDB数据页结构示例分析
通过前面各小节的介绍,相信读者对 InnoDB存储引擎的数据页已经有了一个大致的了解。本小节将通过一个具体的表,结合前面小节所介绍的内容来具体分析一个数据页的内部存储结构。首先建立一张表t,并导入一定量的数据:
接下来用py_innodb_page_info来分析t.ibd,得到如下内容:
可以发现第四个页(page offset 3)是数据页,然后通过hexdump来分析.ibd文件打开整理得到的十六进制文件,数据页从0x0000c000 (16K*3=0xc000)处开始,得到以下内容:
(1)File Header
先来分析前面File Header的38字节:
- 52 1b 24 00,数据页的Checksum值。
- 00 00 00 03,页的偏移量,从0开始。
- ff ff ff ff,前一个页,因为只有当前一个数据页,所以这里为0xffffffff。
- ff ff ff ff,下一个页,因为只有当前一个数据页,所以这里为0xffffffff。
- 00 00 00 0a 6a e0 ac 93,页的LSN。
- 45 bf,页类型,0x45bf 代表数据页。
- 00 00 00 00 00 00 00,这里暂时不管该值。
- 00 00 00 dc,表空间的SPACE ID。
(2)File Header
不急着看下面的Page Header部分,先来观察File Trailer部分。因为File Trailer通过比较File Header部分来保证页写人的完整性。File Trailer的8字节为:
95 ae 5d 39 6a e0 ac 93
- 95 ae 5d 39,Checksum值,该值通过checksum函数与FileHeader部分的checksum值进行比较。
- 6a e0 ac 93,注意该值和FileHeader部分页的LSN后4个值相等。
(3)Page Header
接着分析56字节的Page Header部分。对于数据页而言,Page Header部分保存了该页中行记录的大量细节信息。分析后可得:
-
PAGE_N_DIR_SLOTS=0x001a,代表Page Directory有26个槽,每个槽占用2字节,我们可以从0x0000ffc4到0x0000ff7中找到如下内容:
-
PAGE_HEAP_TOP=0x0dc0代表空闲空间开始位置的偏移量,即0xc000+0x0dc0=0xcdc0处开始,观察这个位置的情况,可以发现这的确是最后一行的结束,接下去的部分都是空闲空间了。
-
PAGE_N_HEAP=0x8066,当行记录格式为Compact时,初始值为0x0802;当行格式为Redundant时,初始值是2。其实这些值表示页初始时就已经有Infinimun和Supremum的伪记录行,0x8066-0x8002=0x64,代表该页中实际的记录有100条记录。
-
PAGE_FREE=0x0000代表可重用的空间首地址,因为这里没有进行过任何删除操作,故这里的值为0。
-
PAGE_GARBAGE=0x0000代表删除的记录字节为0,同样因为我们没有进行过删除操作,这里的值依然为0。
-
PAGE_LAST_INSERT=0x0da5,表示页最后插人的位置的偏移量,即最后的插入位置应该在0xc0000+0x0da5=0xcda5,查看该位置:
可以看到的确是最后插入a列值为100的行记录,但是这次直接指向了行记录的内容,而不是指向行记录的变长字段长度的列表位置。
-
PAGE_DIRECTION=0x0002, 因为通过自增长的方式进行行记录的插人,所以PAGE_ DIRECTION 的方向是向右,为0x00002。
-
PAGE_N_DIRECTION=0x0063,表示一个方向连续插人记录的数量,因为我们是自增长的方式插人了100条记录,因此该值为99。
-
PAGE_N_RECS=0x0064,表示该页的行记录数为100, 注意该值与PAGE_N_HEAP的比较,PAGE_N_HEAP 包含两个伪行记录,并且是通过有符号的方式记录的,因此值为0x8066。
-
PAGE_LEVEL=0x00,代表该页为叶子节点。因为数据量目前较少,因此当前B+树索引只有一层。B+数叶子层总是为0x00。
-
PAGE_ INDEX_ ID=0x000000000001ba, 索引ID.
(4)Infimum 和 Supremum Record
前面提到过InnoDB存储引擎有两个伪记录,用来限定行记录的边界,接着往下看:
观察0xc05E到0xc077,这里存放的就是这两个伪行记录,在InnoDB存储引擎中设置伪行只有一个列,且类型是Char (8)。伪行记录的读取方式和一般的行记录并无不同,我们整理后可以得到如下结果:
然后来分析infimum行记录的recorder header部分,最后两个字节位00 1c表示下一个记录的位置的偏移量,即当前行记录内容的位置0xc063+0x001c,即0xc07f。0xc07f应该很熟悉了,之前分析的行记录结构都是从这个位置开始,如:
可以看到这就是第一条实际行记录内容的位置了,整理后我们可以看到:
通过Recorder Header的最后两个字节记录的下一行记录的偏移量就可以得到该页中所有的行记录,通过Page Header的PAGE_PREV 和PAGE_NEXT就可以知道上个页和下个页的位置,这样InnoDB存储引擎就能读到整张表所有的行记录数据。
(5)PageDirectory
最后分析PageDirectory,前面已经提到了从0x0000ffc4到0x0000ff7是当前页的Page Directory,如下:
需要注意的是,Page Directory是逆序存放的,每个槽占2字节,因此可以看到00 63是最初行的相对位置,即0xc063;0070就是最后一行记录的相对位置,即
0xc070。我们发现这就是前面分析的Infmum和Supremum的伪行记录。
Page Directory槽中的数据都是按照主键的顺序存放的,因此查询具体记录就需要通过部分进行。前面已经提到InnoDB存储引擎的槽是稀疏的,故还需通过RecorderHeader的n_owned进行进一步的判断,如InnoDB存储引擎需要找主键a为5的记录,通过二叉查找PageDirectory的槽,可以定位记录的相对位置在00 e5处,找到行记录的实际位置0xc0e5.
可以看到第一行的记录是4,不是我们要找的6,但是可以发现前面的5字节的Record Header为04 00 28 00 22。找到4~ 8位表示n_owned值得部分,该值为4,表示该记录有4个记录,因此还需要进一步查找,通过recorder header最后两个字节的偏移最0x0022找到下一条记录的位置0xc107,这才是最终要找的主键为5的记录。
Named File Formats 机制
长字符类型字段的存储。这些新的页数据结构和之前版本的页并不兼容,因此从 InnoDB 1.0.x版本开始,InnoDB存储引通过 Named File Formats机制来解决不同版本下页结构兼容性的问题。
InnoDB存储引擎将1.0.x 版本之前的文件格式(file format)定义为 Antelope,将这个版本支持的文件格式定义为 Barracuda。新的文件格式总是包含于之前的版本的页格式。下图显示了Barracuda 文件格式和 Antelope 文件格式之间的关系,Antelope文件格式有Compact 和 Redudant 的行格式,Barracuda 文件格式既包括了 Antelope 所有的文件格式,另外新加入了之前已经提到过的 Compressed和 Dynamic行格式。
参数 innodb_file_format
用来指定文件格式,可以通过下面的方式来查看当前所使用的InnoDB存储引擎的文件格式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VeUsmhqK-1671369274496)(null)]
参数 innodb_filc_format_check
用来检测当前 InnoDB存储引擎文件格式的支持度,该值默认为 ON,如果出现不支持的文件格式,用户可能在错误日志文件中看到类似如下的错误: