MySQL数据库—InnoDB行存储格式

目录

      • 一、InnoDB支持的行存储格式
      • 二、Compact记录格式
        • 1、变长字段长度列表
          • (1)对varchar类型字段长度的存储
          • (2)对char类型字段长度的存储
        • 2、NULL标志位
        • 3、记录头信息
        • 4、RowID
        • 5、Transaction ID和Roll Point
        • 6、列数据
        • 7、一完整行的存储例子
      • 三、Compact记录格式下的行溢出
      • 四、其他行记录格式
        • 1、Redundant
        • 2、Compressed和Dynamic

一、InnoDB支持的行存储格式

InnoDB之前版本为Antelope文件格式,支持Compact和Redundant行记录格式。在InnoDB 1.0.X版本开始引入新的文件格式Barracuda,除了支持以上两种记录格式外,还支持Compressed和Dynamic。即InnoDB支持以下四种行记录格式:

  • Compact
  • Redundant
  • Compressed
  • Dynamic

Compact是较为经典的行记录格式,后出现的两种记录格式也是在Compact的基础上改进优化,如对BLOB数据采用完全溢出方式存储,Compressed会对存储进行压缩等。所以主要介绍Compact存储格式。

在mysql5.7中,默认开启每个表有独立表空间的配置,即对于创建每一个数据表,在mysql的存储目录下,都对应一个.ibd文件记录该表的数据信息。以下两条命令在下面的例子中有用。

-- 查看mysql的数据存储目录
show global variables like "%datadir%" 
-- 在目录下将.ibd文件转换为16进制文件查看
hexdump -C -v mytest.ibd > mytest.txt

二、Compact记录格式

以下的内容与《MySQL技术内幕—InnoDB存储引擎》书中的介绍稍有出入,推测是版本的问题。以下的结论仅是在ubuntu系统下,使用MySQL5.7版本测试所得。如有问题欢迎讨论。

对于Compact记录的格式如下:
MySQL数据库—InnoDB行存储格式_第1张图片

1、变长字段长度列表

下文的所提到的长度均是指字节的长度

该部分存放所有非NULL变长字段(以varchar为代表)的字节长度。如果没有非NULL变长字段,则省略这部分。

需要注意的时,这部分是以逆序的方式记录长度的(涉及大端、小端存储问题),如03 02 01代表的是第一个变长字段长度为1,第二个变长字段长度为2,第三个变长字段长度为3.至于变长字段怎么确定的序列,是根据创建的先后来规定的,即第一个创建的变长字段为第一个变长字段。

(1)对varchar类型字段长度的存储

针对varchar类型,mysql规定在一个表在声明时,所有varchar类型的理论上长度总和不能超过65535(还存在额外的开销,实际要小于这个值,为65532,这里姑且认为上限为65535)。如果超过65535并且没有将SQL_MODE设为严格模式,将会把varchar类型自动转为text类型。

-- 即创建表时,以下设置是非法的,因为a和b两个varchar字段的长度和大于65535
create table tab1(
	a varchar(30000),
	b varchar(40000)
);
-- 以下创建则可以成功
create table tab1(
	a varchar(30000),
	b varchar(30000)
);

由此,可以推算任何一个varchar字段最大长度不会超过65535,即用2字节(16bit)就足以表示它的长度。所以在变长字段长度列表中,对每一个变长字段长度的存储,用1或2个字节。经过测试,得到以下的结论:

  • 如果声明varchar时,长度设为255及以下,则对这个字段的长度用1个字节表示(1-127)
  • 如果声明varchar时,长度设为256及以上,则如果实际存放长度为127及以下,用1个字节表示。否则,若长度为128,则用两个字节表示为0x8080,类推若长度为129,则用两个字节表示为0x8081,直到长度为8098,表示为0x9fa2。当长度为8099及以上时,会发生行溢出(即一页中放不下这行的数据了),这时的长度都表示为0xc314。如图更加清楚表示
    在这里插入图片描述
    这样表示的好处也很明显,即读到一个字节大于等0x80的话,说明这里是用了2位字节表示长度。读到0x3c14的话,说明出现了行溢出现象,需要到其他页继续读这个字段。
  • 举个例子,如果变长字段长度列表为03 94 91 14 c3,则代表第一个变长字段溢出,还保存在其他页一部分;第二个变长字段长度由2字节表示(因为0x91>0x80),长度为0x9194 - 0x8080 + 0x0100 = 4500(10);第三个变长字段长度由1字节表示,为3.
(2)对char类型字段长度的存储

因为在不同的编码下,字符和字节的对应是不同的,比如GBK下,一个汉字由两个字节表示,UTF-8下是三个字节表示。而在mysql4.X中,就规定了不管是varchar(x)还是char(x)这里的x都表示字符数。这就带来了一个问题,char的长度也是不固定的了。实际证明,在Compact行记录格式中,对于字符字节不是1:1的编码方式下,将char也视为变长度字段。以声明字段为char(10)为例,即这个字段可以存10个字符

  • 如果存的字节数小于等于10,则在变长字段长度列表中记录该字段字节长度0x0a,并在后面的该列数据记录值并以0x20(空格)填充至10字节大小。
  • 如果存的字节数大于10,则在变长字段长度列表中记录该字段长度为实际字节长度。
  • 举个例子,在UTF-8编码下,若字段为"abc",则记录长度为0x0a,值为61 62 63 20 20 20 20 20 20 20;若字段为"编码编码编码",则记录长度为0x12,值为e7 bc 96 e7 a0 81 e7 bc 96 e7 a0 81 e7 bc 96 e7 a0 81

2、NULL标志位

  • NULL标志位所占字节数由可为空的字段个数所决定,一个bit位对应一个可为空字段
  • 如01 FE 对应二进制 1,1111,1110 则对应的第2,3,4,5,6,7,8,9个可为空字段为null
  • 若表中没有可为NULL的字段,则NULL标志位可以省略;但如果定义了可为NULL的字段,实际在本行中没有NULL值,则仍需要将NULL标志位设为0,至于NULL标志位的字节数,仍由可为NULL的字段数决定。
    如可为NULL的字段为9个,但在某条记录中没有NULL,则在表示这条记录的时候,需要将NULL标志位置为00 00,占两个字节。
  • 因为有NULL标志位的存在,所以字段若为NULL值,是不占用任何空间存储的。

3、记录头信息

记录头信息共占40bit,5字节空间,具体每个bit位代表的内容如下图所示。
MySQL数据库—InnoDB行存储格式_第2张图片

  • delete_flag的存在说明在执行delete操作删除记录时,不会立马删除这条记录(立马删除会因调整B+树的结构而十分影响性能),而是把这个标志位置为1,经测试还会把该条记录的next_record值置0.
  • next_record记录下一条记录的相对位置(该条记录的起始地址加上这个值即为下一条记录的起始地址)。所以每一条记录之间是类似链表的方式存放的。
  • n_owned涉及到槽的概念,在 MySQL数据库—InnoDB数据页结构中的Page Directory部分有介绍

4、RowID

每一条记录都会有一个RowID,

  • 如果这个表声明了主键,则将主键的值放在RowID位置(大小为主键实际大小);
  • 如果没有主键,则会将第一个定义的非空唯一索引(InnoDB会将唯一约束自动设为索引列)当做RowID放在这个位置(大小为非空唯一键实际大小);
  • 如果没有主键也没有非空唯一键,则自动生成一个自增的6字节指针作为RowID。

5、Transaction ID和Roll Point

Transaction ID固定6字节
Roll Point固定7字节
这两个部分与InnoDB实现MVCC有关,版本控制、事务回滚等内容,这里不详述。

6、列数据

这里即按相应编码方式,按顺序存放个列的数据。在上面已经提到,这里会出现行溢出的现象。

7、一完整行的存储例子

在InnoDB下使用Compact行记录格式,utf8编码。
表中共有5个字段(t1-t5),类型如图所示,无其他约束。
存入如图所示的一行记录,在.ibd文件中查出这一行的十六进制存储内容,如图。
MySQL数据库—InnoDB行存储格式_第3张图片

三、Compact记录格式下的行溢出

上面有提到,在变长字段长度列表中,当长度大于8098时,其长度记录为0xC314,这是发生了行溢出而至。InnoDB页的大小默认为16k,若要保证在一页中是一个树的结构,必须要至少两行记录

而经过测试,一行中的varchar字段的实际总长度的阈值为8098字节,所有varchar字段实际存的超过8098字节,会发生行溢出。

在本页中会优先存储后面的字段,所以从后往前,第一个超过8098字节的字段以及其前面的字段,都只在本页中保留768字节的前缀和20个字节的偏移量(指向行溢出页地址)。

举个例子,如下的表中:

CREATE TABLE mytest1 ( 
t1 VARCHAR ( 5000 ), 
t2 VARCHAR ( 5000 ),
t3 VARCHAR ( 5000 ) ) CHARSET = latin1 ROW_FORMAT = compact;

插入一行数据:

INSERT INTO mytest1
VALUES
(
		REPEAT( 'a', 4500 ),
		REPEAT( 'b' ,4500),
		REPEAT( 'c', 4500 )
);

这时,在一页中会保存4500个c,768个b和768个a。(因为在保存b时4500+4500>8098了)。通过观察mytest1.ibd验证一下:

MySQL数据库—InnoDB行存储格式_第4张图片
通过py_innodb_page_info.py脚本查看页的使用情况:
MySQL数据库—InnoDB行存储格式_第5张图片
查看变长字段(从0000c078开始)长度列表为94 91 14 c3 14 c3 ,所以

在B-tree Node页中:
t3长度为0x9194=4500(10),即保存了4500个c,
t2和t1为0xc314,溢出,即在本页保存了768个b和768个a

而其余的b和a分别存在了两个Uncompressed BLOB Page中。
MySQL数据库—InnoDB行存储格式_第6张图片

四、其他行记录格式

1、Redundant

Redundant格式是MySQL5.0之前InnoDB使用的行记录存储方式,其格式如图所示:


其中,字段长度偏移列表记录了所有字段的相对位置信息。记录头信息也较Compact略微有变化

MySQL数据库—InnoDB行存储格式_第7张图片

2、Compressed和Dynamic

MySQL 5.7版本默认使用Dynamic格式,这两种记录格式与Compact一致,而在Compact的基础上,对行溢出时做了改进。即对于存放BLOB、大varchar等导致行溢出的字段时,采取的是完全行溢出方式。对溢出字段不再在B-tree Node页中存放768个前缀字节,而是只保存20个字节的Uncompressed BLOB Page的偏移量,所有的数据均保存在Uncompressed BLOB Page页中。

此外compressed存储行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据都能够进行有效的存储。

你可能感兴趣的:(MySQL)