数据行是什么?


平时增删改查操作对应的数据,就是数据行,本文写的数据行更多的是针对这些数据在物理存储时的数据。数据行对应着表里面的每一行数据,这些数据行会按照一定的格式存储进磁盘文件。多个数据行是一个连接一个紧挨着存储的,多个数据行组成数据页

数据行的存储格式


在创建表的时候可以指定数据行的存储格式,也可以在创建后修改。

  1. -- 创建表的时候指定数据行的格式为COMPACT
  2. CREATE TABLE table_name (clumns) ROW_FORMAT=COMPACT
  3. -- 修改表的数据行的格式
  4. ALTER TABLE table_name ROW_FORMAT=COMPACT

这里指定的行的存储格式是 COMPACT , 另外还有 Redundant , Compressed 与 Dynamic 格式,内容都差不多。指定了数据行的存储格式为 COMPACT 后,数据行在实际物理存储的时候,就会大概按照以下的格式存储:

变长字段的长度列表 null 值列表 数据头 列1的值 列2的值 …… 列n的值

每个数据行存储的时候,除了存储每一列的值以外,还需要存储一些附加的信息,来描述这些列的数据值,才可以正确高效的读写这些对应的数据行。这些描述信息主要解决的是如何读写变长的字段,和 null 值的处理。

如何读写变长字段 ?


字段的大多数类型都是有指定的长度的,但是也会存在一些可变长度的数据类型,例如 VARCHAR 。VARCHAR 指定了数据的最长的长度,但是实际存储的时候,可以写入小于等于指定长度的数据。例如一个 VARCHAR(10) 的字段,可以写入字符串”1234567890” ,也可以写入”123”。读取这些数据的时候,如果不知道这个值的长度信息,就不能准确的读取出对应的字段的值。

例如,有一个数据包含3个字段,VARCHER(10) , CHAR(1) , CHAR(1) ; 存储的行可能是(”1234”,”5”,”6”), 也可能是(”12”,”5”,”6”)。由于第1列的类型是可变的长度,所以,没有额外的标记第一行的信息的话,是无法读取出第1列是”1234” 或是”1”或是”12”。这时候,就引入了一个额外的信息:变长字段的长度列表

变长字段的长度列表,存储着变长字段的长度,有了这个长度,就可以准确的读取出对应的变长字段的值了。引入了变长字段的长度列表后,上述的数据行,存储大概就变成了这样子:

4 null 值列表 数据头 1234 5 6

在写入这行数据的时候,会将第1列的长度写入变长字段列表中。读取数据的时候,发现第1列是变长类型,然后找到第1列的变长类型的长度是4,然后读取第1列数据”1234”,然后读取第2列,第2列的类型是CHAR(1) , 读取”5” , 读取第3列,第3列的类型是CHAR(1) , 读取”6”。

如果有多列是变长字段也没问题; 例如有数据格式为 VARCHER(10) , VARCHER(10), CHAR(1) ,值为(”123”,”45678”,”9”),存储的时候内容如下:

0x05 0x03 null 值列表 数据头 123456789

变长字段的长度列表是用16进制的数值存储的,并且存储的时候是对字段逆序存储的,列的值之间是没有任何分隔的。
所以读取的过程为,读取第1列类型是VARCHAR,长度是3,读取”123”, 读取第2列类型是VARCHAR,长度是5,读取”45678”,读取第3列,类型是CHAR(1),直接读取1个长度的字符串”9”。

如何存储NULL字段 ?


在存储的时候,如果字段允许为 NULL ,并且这个字段的值是 NULL ,是不会将 “NULL” 写入磁盘的。因为这样就浪费了很多没必要的空间,所以设计采用了二进制的 bit 位来记录数据行的 NULL 值。

NULL 值列表里面,每个允许为 NULL 的字段,占1个位,该字段的值是 NULL ,bit 值就是1 ,如果该字段的值不是 NULL,则 bit 值就是0。

例如一个产品表:

CREATE TABLE product (
    name VARCHAR(20) NOT NULL,
  type VARCHAR(10)
  brand VARCHAR(10)
  company VARCHAR(10)
  summary VARCHAR(50)
)

这个产品有5个字段,名字不能为 NULL , 类型、品牌、公司和简介都可以为 NULL。例如这边表有这个数据(”IPHONE13”,NULL,”苹果”,NULL,”最新苹果手机”); 允许为空的字段有4个,并且第1个和第3个的值为 NULL 。那么它的 NULL 值列表就应该是 1010,实际上 NULL 值列表也是逆序存放的,并且 bit 位最低是8的倍数,如果不够,则高位补0。 所以这个 NULL 值列表应该是这样的: 00000101 。整行数据存储格式如下:

0x06 0x02 0x08 00000101 数据头 IPHONE13苹果最新苹果手机

读取数据的过程为:读取第1个字段,字段类型是可变的,并且不允许为 NULL 的,读取长度8,读取字符串”IPHONE13” ;读取第2个字段”type”,去 NULL 值列表查看 , 值为 NULL , 不读取;读取第3个字段,允许为 NULL,去 NULL 值列表查看,发现值不为 NULL ,并且是变长字段,读取长度2 , 读取字符串”苹果”。就是按照这些信息,就可以将整行数据读取出来了。

40个bit位的数据头


数据行里面还有一个数据头的信息,它是一个40位的bit信息。

第1和第2位,是预留信息没有意义。
第3位是删除标记。delete_mask , 值为1,表示数据已经被删除;数据被删除的话,也不是立刻就被清理掉的,而是先标记,然后统一删除。
第4位是 min_rec_mask , 意思是在 B+ 树里每一层的非叶子结点里的最小值都有这个标记。
第5~8位是 n_owned , 一个记录数。
第9~21位是 heap_no , 当前这行数据在记录堆里面的位置。
第22~24位是 record_type , 这行数据的类型:0 普通类型,1 B+树的非叶子结点,2 最小值数据,3 最大值数据。
第25~40位是 next_record , 指向当前数据行的下一个数据的指针。

数据行的真实存储


数据行正式进行物理存储的时候,是不会将字符串直接存储到物理文件上的,而是对字符串按照一定的规则进行编码之后进行存储。
并且在存储的时候,会在列值之前加入几个附加的列的信息:
DB_ROW_ID : 数据行的唯一标识,不是数据的主键,而是数据库自己生产的唯一标识。当数据没有主键和唯一索引的时候,会自动使用这个值作为主键。
DB_TRX_ID : 事务ID,标明了当前数据行最后是被哪个事务更新的。
DB_ROLL_PTR : 回滚指针,用于进行事务回滚。

综上所述,以上面的例子为例,

0x06 0x02 0x08 00000101 数据头 IPHONE13苹果最新苹果手机

实际上物理存储的行的内容如下:

变长字段的长度列表 null 值列表 40位数据头 DB_ROW_ID DB_TRX_ID DB_ROLL_PTR 列1的值 列2的值 列3的值
0x06 0x02 0x08 00000101 0100010..0100….010 E5615 A515454 DA2565 6465461 5464655 1446686

行溢出


数据行都是放在数据页里面存储的,数据页默认为 16K 当一行数据行的长度超过了这个数据页的长度,就会产生行溢出。发生行溢出的时候,还是会在当前发生行溢出的数据页存储当前的数据,但是仅包含一部分数据,同时包含一个20个字节的指针,指向下一个数据页,在下一个数据页里面存储着另外的一部分当前行的数据,如果还是装不下,则会继续使用下一个数据页来存储,这样使用数据页链接起来存储大数据行。