行数据存储

对数据页中的每一行数据,他在磁盘上是怎么存储的?其实这里涉及到一个概念,就是行格式。我们可以对一个表指定他的行存储的格式是什么样的,比如我们这里用一个COMPACT格式。
CREATE TABLE table_name (columns) ROW_FORMAT=COMPACT ALTER TABLE table_name ROW_FORMAT=COMPACT
你可以在建表的时候,就指定一个行存储的格式,也可以后续修改行存储的格式。这里指定了一个COMPACT行存储格式,在这种格式下,每一行数据他实际存储的时候,大概格式类似下面这样: 变长字段的长度列表,null值列表,数据头,column01的值,column02的值,column0n的值......对于每一行数据,他其实存储的时候都会有一些头字段对这行数据进行一定的描述,然后再放上他这一行数据每一列的具体的值,这就是所谓的行格式。除了COMPACT以外,还有其他几种行存储格式,基本都大同小异。
 
一行数据在磁盘文件里存储的时候,实际上首先会包含自己的变长字段的长度列表,然后是NULL值列表,接着是数据头,然后接着才是真实数据
0x09 0x04 NULL值列表 头信息 school =xx_school name=jack

变长字段磁盘存储方式

在MySQL里有一些字段的长度是变长的,是不固定的,比如VARCHAR(10)之类的这种类型的字段,实际上他里面存放的字符串的长度是不固定的,有可能是“hello”这么一个字符串,也可能是“a”这么一个字符串
现在有一行数据,他的几个字段的类型为VRACHAR(10),CHAR(1),CHAR(1),那么他第一个字段是VARCHAR(10),这个长度是可能变化的,所以这一行数据可能就是类似于:hello a a,这样子,第一个字段的值是“hello”,后面两个字段的值都是一个字符a 。另外一行数据,同样也是这几个字段,他的第一个字段的值可能是“hi”,后面两个字段也是“a”
最终落地到磁盘里的时候, hello a a hi a a 挨着存储,从磁盘读取数据的时候,不知道取多少个长度的数据, 可能读取出来“hello a a hi”是一行数据,也可能读取出来“hello a”
 
MySQL 在存储每一行数据的时候,都保存一下他的变长字段的长度列表,这样才能解决一行数据的读取问题
“hello a a”前面补充一些额外信息,首先就是变长字段的长度列表,你会看到这行数据在磁盘文件里存储的时候,其实是类似如下的格式:0x05 null值列表 数据头 hello a a
多个变长字段,如何存放他们的长度?
比如一行数据有VARCHAR(10) VARCHAR(5) VARCHAR(20) CHAR(1) CHAR(1),一共5个字段,其中三个是变长字段,此时假设一行数据是这样的:hello hi hao a a
此时在磁盘中存储的,必须在开头的变长字段长度列表中存储几个变长字段的长度,一定要注意一点,这里是逆序存储的! 也就是说先存放VARCHAR(20)这个字段的长度,然后存放VARCHAR(5)这个字段的长度,最后存放VARCHAR(10)这个字段的长度。所以一行数据实际存储可能是下面这样的: 0x03 0x02 0x05 null值列表 头字段 hello hi hao a a
 

多个NULL字段存储方式

磁盘上有特殊的数据区域存储null值,这块区域就是NULL值列表。
NULL值是以二进制bit位来存储的
对所有的NULL值,不通过字符串在磁盘上存储,而是通过二进制的bit位来存储,一行数据里假设有多个字段 的值都是NULL,那么这多个字段的NULL,就会以bit位的形式存放在NULL值列表中。
举个例子,假设你有一张表,他的建表语句如下所示:
CREATE TABLE customer ( name VARCHAR(10) NOT NULL, address VARCHAR(20), gender CHAR(1), job VARCHAR(30), school VARCHAR(50) ) ROW_FORMAT=COMPACT;
假设这个表里有一行数据: “jack NULL m NULL xx_school”
只有name和school两个变长字段是有值的,把他们的长度按照逆序放在变长字段长度列表中就可以了,如下所示
0x09 0x04 NULL值列表 头信息 school =xx_school name=jack
只要是允许为NULL的字段,都有一个bit位的值,如果bit值是1说明是NULL,如果bit值是0说明不是NULL
比如上面4个字段都允许为NULL,每个人都会有一个bit位,这一行数据的值是jack NULL m NULL xx_school,其中2个字段是null,2个字段不是null,所以4个bit位应该是:1010 ,实际放在NULL值列表的时候,他是按逆序放的,所以在NULL值列表里,放的是:0101 不足8个bit位就高位补0,实际存放如下
0x09 0x04 00000101 头信息 school =xx_school name=jack
 

40bit数据头

每一行数据存储的时候,还得有40个bit位的数据头,这个数据头是用来描述这行数据的。
这40个bit位里,第一个bit位和第二个bit位,都是预留位,是没任何含义的。接下来有一个bit位是delete_mask,他标识的是这行数据是否被删除了,下一个bit位是min_rec_mask 接下来有4个bit位是n_owned 接着有13个bit位是heap_no,他代表的是当前这行数据在记录堆里的位置 。 然后是3个bit位的record_type,这就是说这行数据的类型: 0代表的是普通类型,1代表的是B+树非叶子节点,2代表的是最小值数据,3代表的是最大值数据 。
最后是16个bit的next_record,这个是指向它下一条数据的指针
 

实际数据的存储

之前说了一个例子,有一行数据是“jack NULL m NULL xx_school”,那么他真实存储大致如下所示:0x09 0x04 00000101 0000000000000000000010000000000000011001 jack m xx_school 刚开始先是他的变长字段的长度,用十六进制来存储,然后是NULL值列表,指出了谁是NULL,接着是40个bit位的数据头,然后是真实的数据值,就放在后面。
读取这个数据的时候,他会根据变长字段的长度,先读取出来jack这个值,因为他的长度是4,就读取4个长度的数据 ,然后发现第二个字段是NULL,就不用读取了。
第三个字段是定长字段,直接读取1个字符就可以了,就是m这个值,第四个字段是NULL,不用读取了;第五个字段是变长字段长度是9,读取出来xx_school。
 
真正在磁盘上存储的时候, 数据是根据我们数据库指定的字符集编码,进行编码之后再存储的0x09 0x04 00000101 0000000000000000000010000000000000011001 616161 636320 6262626262
在实际存储一行数据的时候,会在他的真实数据部分,加入一些隐藏字段 。
首先有一个DB_ROW_ID字段,这就是一个行的唯一标识,是他数据库内部给你搞的一个标识,不是你的主键ID字段。如果我们没有指定主键和unique key唯一索引的时候,他就内部自动加一个ROW_ID作为主键。
接着是一个DB_TRX_ID字段,这是跟事务相关的,他是说这是哪个事务更新的数据,这是事务ID。
最后是DB_ROLL_PTR字段,这是回滚指针,是用来进行事务回滚的。
 
实际一行数据可能看起来如下所示:
0x09 0x04 00000101 0000000000000000000010000000000000011001 00000000094C(DB_ROW_ID)00000000032D(DB_TRX_ID) EA000010078E(DB_ROL_PTR) 616161 636320 6262626262

行溢出

一行数据存储的内容太多,导致磁盘一个数据页都放不下了,此时只能溢出这个数据页,把数据溢出存放到其他数据页里去,那些数据页就叫做溢出页。
 
比如有一个表的字段类型是VARCHAR(65532),意思就是最大可以包含65532个字符,那也就是65532个字节,这就远大于16kb的大小了,也就是说这一行数据的这个字段都远超一个数据页的大小了!这个时候实际上会在那一页里存储你这行数据,然后在那个字段中,仅仅包含他一部分数据,同时包含一个20个字节的指针,指向了其他的一些数据页,那些数据页用链表串联起来,存放这个VARCHAR(65532)超大字段里的数据。
行溢出
行溢出
其他的一些字段类型都是一样的,比如TEXT、BLOB这种类型的字段,都有可能出现溢出,然后一行数据就会存储在多个数据页里。