mysql InnoDB数据页行格式

之前写过两篇mysql相关的的学习笔记,分别是关于字符集和比较规则InnoDB逻辑存储结构,本篇主要是记录一下InnoDB存储引擎的行格式方面的知识

行格式的几种类型: compact、redunct、compress、dynamic

在InnoDB 1.0.x版本之前,InnoDB存储引擎提供了Compact和Redunct两种格式来存放行记录数据,这两种也叫Antelope文件格式。InnoDB 1.0.x版本开始引进了Compress和Dynamic两种新的行记录格式,这两种也叫Barracuda文件格式。

使用命令show variables like "innodb_file_format";可以查看当前数据库默认使用的文件格式。

示例:


mysql> show variables like "innodb_file_format"; 


+--------------------+----------+
| Variable_name      | Value    |
+--------------------+----------+
| innodb_file_format | Antelope |
+--------------------+----------+
1 row in set (0.01 sec) 

使用命令show table status like "myTableName%"\G可以查看表所使用的行格式。

先说一下compact这种行格式

一、Compact行格式

插入单行compact格式的大致包括

compact格式大致包括这些内容:

  • 变长字段列长度:记录的是非NULL变长字段长度的逆序

    • 若列的长度小于255字节,用1个字节表示列的长度;若大于255字节,用2字节表示列的长度

    • 在数据库中,varchar(M)、text、blob都是变长类型

  • NULL标志位:用bit位表示该列是否为NULL,若是NULL则该bit为1。是各个列是否为NULL逆序表示,前面会填充0直到凑满整数个字节

  • 记录头(record header):固定占5个字节

  • 列数据

    • NULL列不占该部分的空间

    • 会默认添加两个隐藏列:事务ID列(默认6字节)、回滚指针列(默认7字节)

    • 如果该表没有主键,还会添加一个rowid列(默认6字节)。如果有主键,则字节数为主键类型字节数。

1.1 记录头(record header)

上面提到记录头(record header)默认占5个字节,具体的bit位表示如下:

record header具体bit位的表示

可以看到record header里面最后两个字节表示next_record表示的是下一条记录的相对位置,这样的话,类似一个单链表把表里的行串了起来,如下图所示。

插入多行compact格式--包括record header的指针


二、以ascii字符集的表举例说明

下面为了方便用ascii字符集来具体举例说明Compact行格式

CREATE TABLE `testformat` (
  `c1` int UNSIGNED NOT NULL AUTO_INCREMENT,
  `c2` varchar(255),
  `c3` char(11),
  `c4` varchar(255),
  PRIMARY KEY (`c1`)
) ENGINE=InnoDB DEFAULT CHARSET=ascii ROW_FORMAT=COMPACT;

创建表,指定表的字符集为ascii

往表里插入几条数据之后,查看这个表里面的内容:

mysql> select * from testformat;
+----+------+------+------+
| c1 | c2   | c3   | c4   |
+----+------+------+------+
|  1 | a    | bbbb | cc   |
|  2 | bb   | NULL | NULL |
|  3 | cc   | NULL | dd   |
|  4 | dddd | e    | NULL |
+----+------+------+------+
4 rows in set (0.00 sec)

2.1 查看数据页是如何存储的

show table status 查看表的row format信息

show variables like 'innodb_file_per_table';

+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON    |
+-----------------------+-------+

如果innodb_file_per_table为ON,则表示InnoDB为每一个表建立一个独立表空间

SHOW VARIABLES LIKE 'datadir' 查看数据存放的地方

+---------------+------------------------------------+
| Variable_name | Value                              |
+---------------+------------------------------------+
| datadir       | /usr/local/var/mysql/              |
+---------------+------------------------------------+

进入这个目录,找到并进入testformat这个文件夹,可以看到里面testformat.frmtestformat.ibd

《Mysql技术内幕-InnoDB存储引擎》的作者自己写了小工具,可以大致查看表空间的内容,我在网上找了下,从这个链接找到了

运行命令 py_innodb_page_info.py -v testformat.ibd

此处插入py_innodb_page_info运行后的图片

发现page offset 00000003, page type <B-tree Node>, page level <0000>是数据页

使用命令hexdump -C -v testformat.ibd > testformat.txt把二进制文件的内容导出到了txt文件里面。

使用SHOW GLOBAL STATUS like 'Innodb_page_size'查看当前设置的的大小

+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Innodb_page_size | 16384 |
+------------------+-------+

可以看到默认的页的大小是16K

根据上面py_innodb_page_info.py查出来的东西,数据页应该是从16K * 3 = 49152,转换成16进制就是0xC000, 打开刚刚导出的testformat.txt文件,从0xC000开始看

此处插入图片--hex出来的ibd的16进制的内容

在前一篇笔记InnoDB逻辑存储结构写过一个数据页里面包括几个部分,如下图所示:

此处插入图片--页的结构

可以从图中看到,一个数据页里面,File Header占38个字节,Page Header占56个字节。

所以,我们从0xC000开始往后,跳过File HeaderPage Header找到Infimum+Suprenum Recored开始的地方(即跳过38+56=94个字节),也就是0xC05E

先来看下Infimum+Suprenum Recored存储的样子

在InnoDB存储引擎中设置伪记录行只有一个列,且类型为char(8)

所以,Infimum这个最小伪记录的的行格式如下:

01 00 02 00 1d          // record header ,即记录头信息
69 6e 66 69 6d 75 6d 00 // 即字符串Infimum,多了一个0x00字节

Infimum在16进制里面的样子

Suprenum这个最大伪记录的的行格式如下:

05 00 0b 00 00          // record header,即记录头信息
73 75 70 72 65 6d 75 6d // 即字符串Supremum

Suprenum在十六进制里的样子

2.2 查看第一行是如何存储的

Infimun的记录头信息里面的最后两个字节,表示的是下一条记录的列内容地址的偏移量。

所以我们要找的第一条记录列内容的地址是0xC063 + 0x001d = 0xC080,第一条记录的列内容如下图(图片中红颜色框住的部分)

此处插入图片--hex出来的ibd的16进制的内容--框住第一行

00 00 00 01                      // 隐藏列row_id,因为建表的时候设置c1列为主键,所以此处是c1列的值
00 00 00 eb 01 00                // 隐藏列Transaction ID,即事务ID列,6字节大小
b6 00 00 02 8c 01 10             // 隐藏列Roll Pointer,即回滚指针列,7字节大小
61                               // c2列的值'a'
62 62 62 62 20 20 20 20 20 20 20 // c3列的值'bbbb',因为c3列是char(M)类型,当该列内容长度小于M的时候,默认会用0x20在后面填充
63 63                            // c4列的值'cc'

0xC080往前看,看下第一条记录的变长字段长度列表、NULL标志位、记录头信息的相应内容:

02 01 00 00 00 10 00 26,从0xC078 ~ 0xC07F就是变长字段长度列表、NULL标志位、记录头信息这部分的内容

  • 记录头信息默认是占5个字节,所以是0xC07B ~ 0xC07F这部分地址的内容,也就是00 00 10 00 26

    • 记录头信息里面的最后两个字节,表示的是下一条数据的列内容的偏移量,所以下一条数据的列内容的地址是0xC080 + 0x0026 = 0xC0A6
  • 第一条记录里面没有NULL列,所以NULL标志位的内容是0x00

  • 变长字段长度列表的内容就是02 01,也就是c2、c4列的内容长度的逆序

2.3 查看第二行是如何存储的

下面从0xC0A6这个地址看下第二条记录的列内容(图片中黄颜色框住的部分)

此处插入图片--hex出来的ibd的16进制的内容---框住第二行

00 00 00 02            // 隐藏列row_id,因为建表的时候设置c1列为主键,所以此处是c1列的值
00 00 00 eb 01 01      // 隐藏列Transaction ID,即事务ID列,6字节大小
b7 00 00 05 5f 01 10   // 隐藏列Roll Pointer,即回滚指针列,7字节大小
62 62                  // c2列的值'bb'
                       // c3、c4列都是NULL,所以不占空间

0xC0A6往前看,看下第二条记录的变长字段长度列表、NULL标志位、记录头信息的相应内容:

02 06 00 00 18 00 1b,从0xC09F ~ 0xC0A5就是变长字段长度列表、NULL标志位、记录头信息这部分的内容

  • 记录头信息默认是占5个字节,所以是0xC0A1 ~ 0xC0A5这部分地址的内容,也就是00 00 18 00 1b

    • 记录头信息里面的最后两个字节,表示的是下一条数据的列内容的偏移量,所以下一条数据的列内容的地址是0xC0A6 + 0x001B = 0xC0C1
  • 在表的所有列中,c2、c3、c4的值都可以是NULL,所以要判断这三个列的NULL标志位。第二条记录里面c3和c4列都是NULL而c2不是NULL,所以NULL标志位的内容是0x06,二进制就是00000110,其中最后三位110也就是c2、c3、c4列是否为NULL的逆序,前面的0是为了填充变成整数个字节

  • 在所有列中,c2、c4是变长字段,第二条记录的变长字段长度列表的内容是0x02,因为第二条记录的c4是NULL,所以即使c4列的类型是varchar是变长字段,也是不存储它的长度的。所以0x02就是c2列的内容的长度

2.4 查看第三行是如何存储的

下面从0xC0C1这个地址看下第三条记录的列内容(图片中绿颜色框住的部分)

此处插入图片--hex出来的ibd的16进制的内容---框住第三行

00 00 00 03            // 隐藏列row_id,因为建表的时候设置c1列为主键,所以此处是c1列的值
00 00 00 eb 01 06      // 隐藏列Transaction ID,即事务ID列,6字节大小
ba 00 00 05 61 01 10   // 隐藏列Roll Pointer,即回滚指针列,7字节大小
63 63                  // c2列的值'cc'
                       // c3列是NULL,所以不占空间
64 64                  // c4列的值'dd'  

0xC0C1往前看,看下第三条记录的变长字段长度列表、NULL标志位、记录头信息的相应内容:

02 02 02 00 00 20 00 1c,从0xC0B8 ~ 0xC0C1就是变长字段长度列表、NULL标志位、记录头信息这部分的内容

  • 记录头信息默认是占5个字节,所以是0xC0BC ~ 0xC0C1这部分地址的内容,也就是00 00 20 00 1c

    • 记录头信息里面的最后两个字节,表示的是下一条数据的列内容的偏移量,所以下一条数据的列内容的地址是0xC0C1 + 0x001C = 0xC0DD
  • 在表的所有列中,c2、c3、c4的值都可以是NULL,所以要判断这三个列的NULL标志位。第三条记录里面c3是NULL,所以NULL标志位的内容是0x02,二进制就是00000010,其中最后三位010也就是c2、c3、c4列是否为NULL的逆序,前面的0是为了填充变成整数个字节

  • 在所有列中,c2、c4是变长字段,第三条记录的变长字段长度列表的内容是02 02,因为c2、c4内容分别为ccdd长度都是2。

2.5 查看第四行是如何存储的

下面从0xC0DD这个地址看下第四条记录的列内容(图片中蓝颜色框住的部分)

此处插入图片--hex出来的ibd的16进制的内容---框住第四行

00 00 00 04            // 隐藏列row_id,因为建表的时候设置c1列为主键,所以此处是c1列的值
00 00 00 eb 01 07      // 隐藏列Transaction ID,即事务ID列,6字节大小
bb 00 00 01 cf 01 10   // 隐藏列Roll Pointer,即回滚指针列,7字节大小
64 64 64 64            // c2列的值'dddd'
65                     // c3列的值'e'
                       // c4列的值是NULL,所以不占空间 

0xC0DD往前看,看下第四条记录的变长字段长度列表、NULL标志位、记录头信息的相应内容:

04 04 00 00 28 ff 93,从0xC0D6 ~ 0xC0DC就是变长字段长度列表、NULL标志位、记录头信息这部分的内容

  • 记录头信息默认是占5个字节,所以是0xC0D8 ~ 0xC0DC这部分地址的内容,也就是00 00 28 ff 93

    • 记录头信息里面的最后两个字节,表示的是下一条数据的列内容的偏移量,所以下一条数据的列内容的地址是0xC0DD + 0xFF93 = 0xC070

    • 因为第四条记录是我们看到的表里的最后一条记录,所以它的下一条数据是Supremum。看下文件里面,0xC070果然指向的是Supremum这条伪记录的列内容

  • 在表的所有列中,c2、c3、c4的值都可以是NULL,所以要判断这三个列的NULL标志位。第四条记录里面只有c4列是NULL,所以NULL标志位的内容是0x04,二进制就是00000100,其中最后三位100也就是c2、c3、c4列是否为NULL的逆序,前面的0是为了填充变成整数个字节

  • 在所有列中,c2、c4是变长字段,c3不是变长字段,第四条记录的变长字段长度列表的内容是04,因为c2内容分别为dddd长度是2,而c4列的值是NULL所以此处不记录c4列的长度。


上面举例的时候为了方便,选用了ascii字符集作为表的字符集,如果是变长字符集的表,就不会是这样的了。举例说,创建一个utf8字符集的表(utf8在mysql中最短一个字节最长3个字节),塞入同样的数据。

三、以utf8字符集的表举例说明

CREATE TABLE `testcharsetutf8` (
  `c1` int NOT NULL AUTO_INCREMENT,
  `c2` varchar(255),
  `c3` char(11),
  `c4` varchar(255),
  PRIMARY KEY (`c1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

创建表,指定表的字符集为utf8

往表里插入和上面一样的数据后,查看这个表里面的内容:

mysql> select * from testcharsetutf8;
+----+------+------+------+
| c1 | c2   | c3   | c4   |
+----+------+------+------+
|  1 | a    | bbbb | cc   |
|  2 | bb   | NULL | NULL |
|  3 | cc   | NULL | dd   |
|  4 | dddd | e    | NULL |
+----+------+------+------+
4 rows in set (0.00 sec)

将二进制文件导出来之后,我用不同的颜色表示了不同的内容

此处插入testcharsetutf8的二进制内容-不同颜色

  • 灰色背景:Infimum

    • 可以看到Infimumrecord header最后两个字节是00 1e,所以第一条记录的列内容的地址就是0xC063 + 0x001e = 0xC081
  • 棕色背景:Suprenum

  • 红色背景:表示第一条记录的列内容

    • 根据Infimum的信息得到第一条记录的列内容是从0xC081开始,具体如下。因为上面那个ascii字符集的表创建的时候指定的c1列为int unsigned是无符号的所以row_id列最高位是0,而此处用的utf8字符集的表创建的时候指定的c1列为int是有符号的,所以row_id列最高位用来表示正或负。
      80 00 00 01                      // 隐藏列row_id,因为建表的时候设置c1列为主键,所以此处是c1列的值。
      00 00 00 eb 02 3f                // 隐藏列Transaction ID,即事务ID列,6字节大小
      bf 00 00 02 8c 01 10             // 隐藏列Roll Pointer,即回滚指针列,7字节大小
      61                               // c2列的值'a'
      62 62 62 62 20 20 20 20 20 20 20 // c3列的值'bbbb',因为c3列是char(M)类型,当该列内容长度小于M的时候,默认会用0x20在后面填充
      63 63                            // c4列的值'cc'
    
  • 红色下划线:表示第一条记录的变长字段列长度+NULL标志位+record header,即02 0b 01 00 00 00 10 00 26

    • record header默认占5个字节,即00 00 10 00 26

    • 变长字段列长度+NULL标志位02 0b 01 00

      • 第一条记录没有列是NULL,所以NULL标志位00

      • 在所有列中,c2、c4是变长的,c3不是变长的,但是因为当前是utf8字符集,所以char(M)也被认为是变长字段,所以变长字段列长度02 0b 01是c2、c3、c4长度的逆序

  • 黄色背景:表示第二条记录的列内容

    • 根据第一条记录的record header的最后两个字节00 26计算得到第二条记录的列内容的位置是0xC081 + 0x0026 = 0xC0A7,具体如下
      80 00 00 02            // 隐藏列row_id,因为建表的时候设置c1列为主键,所以此处是c1列的值
      00 00 00 eb 0a 41      // 隐藏列Transaction ID,即事务ID列,6字节大小
      b8 00 00 02 3d 01 10   // 隐藏列Roll Pointer,即回滚指针列,7字节大小
      62 62                  // c2列的值'bb'
                             // c3、c4列都是NULL,所以不占空间
    
  • 黄色下划线:表示第二条记录的变长字段列长度+NULL标志位+record header,即02 06 00 00 18 00 1b

    • record header默认占5个字节,即00 00 18 00 1b

    • 变长字段列长度+NULL标志位02 06

      • 第二条记录中只有c1、c2列不是NULL,其他都是NULL,所以NULL标志位06,最后三位110也就是c2、c3、c4列是否为NULL的逆序,前面的0是为了填充变成整数个字节

      • 在除主键列所有非NULL列中,c2是varchar(M)类型其值是bb即长度为2,所以变长字段列长度02

  • 蓝色背景:表示第三条记录的列内容

    • 根据第二条记录的record header的最后两个字节00 1b计算得到第二条记录的列内容的位置是0xC0A7 + 0x001B = 0xC0C2,具体如下
      80 00 00 03            // 隐藏列row_id,因为建表的时候设置c1列为主键,所以此处是c1列的值
      00 00 00 eb 0a 46      // 隐藏列Transaction ID,即事务ID列,6字节大小
      bb 00 00 01 cf 01 10   // 隐藏列Roll Pointer,即回滚指针列,7字节大小
      63 63                  // c2列的值'cc'
                             // c3列是NULL,所以不占空间
      64 64                  // c4列的值'dd' 
    
  • 蓝色下划线:表示第三条记录的变长字段列长度+NULL标志位+record header,即02 02 02 00 00 20 00 1d

    • record header默认占5个字节,即00 00 20 00 1d

    • 变长字段列长度+NULL标志位02 02 02

      • 第三条记录中c2、c4列不是NULL,所以NULL标志位02,最后三位010也就是c2、c3、c4列是否为NULL的逆序,前面的0是为了填充变成整数个字节

      • 在除主键列所有非NULL列中,c2是varchar(M)类型其值是cc即长度为2,c4是varchar(M)类型其值是dd即长度为2,所以变长字段列长度02 02

  • 绿色背景:表示第四条记录的列内容

    • 根据第三条记录的record header的最后两个字节00 1b计算得到第二条记录的列内容的位置是0xC0C2 + 0x001D = 0xC0DF,具体如下
      80 00 00 04            // 隐藏列row_id,因为建表的时候设置c1列为主键,所以此处是c1列的值
      00 00 00 eb 0a 47      // 隐藏列Transaction ID,即事务ID列,6字节大小
      bc 00 00 05 63 01 10   // 隐藏列Roll Pointer,即回滚指针列,7字节大小
      64 64 64 64            // c2列的值'dddd'
      65                     // c3列的值'e'
                             // c4列的值是NULL,所以不占空间 
    
  • 绿色下划线:表示第四条记录的变长字段列长度+NULL标志位+record header,即0b 04 04 00 00 28 ff 91

    • record header默认占5个字节,即00 00 28 ff 91

    • 变长字段列长度+NULL标志位0b 04 04

      • 第四条记录中只有c4列是NULL,所以NULL标志位04,最后三位100也就是c2、c3、c4列是否为NULL的逆序,前面的0是为了填充变成整数个字节

      • 在除主键列所有非NULL列中,c2是varchar(M)类型其值是dddd即长度为4,c3是char(M)类型其值是e即长度为1,但是因为是utf8字符集,所以c3也被认为是变长的,所以变长字段列长度0b 04即c2、c3长度的逆序


三、行溢出

创建一个含有varchar(65535)列的表

CREATE TABLE `testover` (
  c1 varchar(65535)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=ascii ROW_FORMAT=COMPACT;
 Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual.You have to change some columns to TEXT or BLOBs

实践下来,发现在ascii、latin1这类固定长度的字符集下,varchar(M)的M最大可以为65532。如果超过65532,创建表的时候会报错。

而且65532指的是表中所有varchar的长度总和,例如下面这个预计,表中所有varchar的长度超过了65532,会报错

CREATE TABLE `testover2` (
  c1 varchar(33000),
  c2 varchar(33000)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=ascii ROW_FORMAT=COMPACT;

上面提到过,一个页默认是16K(即16384字节),而varchar(M)的M的取值可以超过16384,这样的话岂不是一个页连一行的内容都存不下?

InnoDB存储引擎可以将一条记录中的某些数据存储在数据页面之外(即溢出页面)。下面看下当varchar(M)很长的时候,是怎么存的

CREATE TABLE `testover` (
  c1 varchar(65532)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=ascii ROW_FORMAT=COMPACT;


insert into testover select repeat('a', 65532);   

看下表空间的内容,可以看到有4个Uncompressed BLOB Page,这里面肯定存放了东西

varchar(65532)表空间内容

先看下B-tree Node那个页面里面放了什么

btree-node-varchar-65532

可以看到从0xC093~0xC392一共存放了768个字节的数据,之后是偏移量,指向行溢出页。

此处插入compact行溢出

那到底多长的varchar会被放到行溢出页去呢?

答: 当一个页里只能放下一条记录的时候,InnoDB存储引擎会自动将行数据放到溢出页面中。对BLOBTEXT类型来说也是,至少保证一个页能存放两条记录。

四、Compressed和Dynamic行记录格式

上一节里面提到Compact行格式在行溢出的时候,会存储前768个字节。而CompressedDynamic这两种格式对于行溢出,只使用了20字节存放指针,指向Off Page,实际的数据全在Off Page中。

此处插入Compressed和Dynamic行溢出

Compressed行记录格式的另外一个功能是,存储在其中的行数据会以zlib算法进行压缩,对于BLOB、TEXT、VARCHAR等大长度类型的数据能够进行非常有效的压缩。

五、Redundant行记录格式

Redundant行记录格式如下图所示

此处插入Redundant行记录格式

  • 长度偏移列表

    • 计算方式是:假设列1、列2、列3长度分别为X、Y、Z,那么长度偏移列表的内容就是(X+Y+Z)、(X+Y)、(X),列长度不超过255的时候用1个字节表示,超过255的时候用2字节表示

    • 对于变长字段且是NULL vs 非变长字段且是NULL,在计算长度偏移的时候不一样。

  • 记录头信息

    • 默认占6个字节。(与Compact不一样,Compact的记录头占5个字节)
  • 列数据

    • Redundant这种行记录格式会将非变长字段的NULL列的值也存起来,并且使用0填充。比如说一个NULL列它是char(10)类型,那么这个列就会占10个字节(ascii字符集)。如果是utf8字符集,那么就占30个字节。

下面来举例说一下:

CREATE TABLE `testRedundant` (
  `c1` int UNSIGNED NOT NULL AUTO_INCREMENT,
  `c2` varchar(255),
  `c3` char(11),
  `c4` varchar(255),
  PRIMARY KEY (`c1`)
) ENGINE=InnoDB DEFAULT CHARSET=ascii ROW_FORMAT=REDUNDANT;
mysql> select * from testRedundant;
+----+------+------+------+
| c1 | c2   | c3   | c4   |
+----+------+------+------+
|  1 | a    | bb   | ccc  |
|  2 | a    | NULL | NULL |
+----+------+------+------+
1 row in set (0.01 sec)

5.1 第一条记录

第一条记录的长度偏移列表+记录头+列数据如下:

20 1d 12 11 0a 04                   // 长度偏移列表
00 00 28 0d 00 74                   // 记录头,6字节
00 00 00 01                         // 隐藏列rowid
00 00 00 eb 0a bd                   // 隐藏列transctionId
8f 00 00 01 b8 01 10                // 隐藏列rollPoint
61                                  // c1列
62 62 20 20 20 20 20 20 20 20 20    // c2列
63 63 63                            // c3列

20 1d 12 11 0a 04表示长度偏移列表,逆序过来就是04 0a 11 12 1d 20,计算方式如下:

  • 隐藏列rowid长度为4

  • 隐藏列transctionId长度为6,即偏移为0x0a=0x04+0x06

  • 隐藏列rollPoint长度为7,即偏移为0x11=0x04+0x06+0x07

  • c2列长度为1,即偏移为0x12=0x11+0x01

  • c3列是char(11)类型,长度为11,即偏移为0x1D=0x12+0x0B

  • c4列是长度为3,即偏移为0x20=0x1D+0x03

5.2 第二条记录

第二条记录的长度偏移列表+记录头+列数据如下:

9d 9d 12 11 0a 04                   // 长度偏移列表
00 00 10 0d 00 74                   // 记录头,6字节
00 00 00 02                         // 隐藏列rowid
00 00 00 eb 0a b4                   // 隐藏列transctionId
89 00 00 01 3f 01 10                // 隐藏列rollPoint
61                                  // c1列
00 00 00 00 00 00 00 00 00 00 00    // c2列为NULL,c2列是char类型
                                    // c3列为NULL,c3列是varchar类型

9d 9d 12 11 0a 04表示长度偏移列表,逆序过来就是04 0a 11 12 9d 9d,计算方式如下:

  • 隐藏列rowid长度为4

  • 隐藏列transctionId长度为6,即偏移为0x0a=0x04+0x06

  • 隐藏列rollPoint长度为7,即偏移为0x11=0x04+0x06+0x07

  • c2列长度为1,即偏移为0x12=0x11+0x01

  • c3列是char(11)类型,长度为11,但它是NULL列,这里与上面不一样,变成了9d

  • c4列是varchar类型,是NULL列, 长度为0,偏移为9d


这篇笔记主要记录了一下自己对行格式相关知识的学习,之后会学习一下InnoDB索引相关的知识。


2021-03-12 更新:

从上面的学习中可以知道数据页里面的一行一行记录是以单链表的形式串起来的。但是上面举的例子有点不太好,上面的表的主键都是自增的,所以可能会带来一个误解,让人误以为串起来的顺序是按照数据插入顺序来的。实际上是按照主键递增的顺序串起来的。

假如我建表的时候,不需要主键自增。我先插入(1, 'aa', 'bbbb', cc),再插入(3, 'cc', NULL, 'dd'),然后插入(4, 'dddd', 'e', NULL),再插入(2, 'bb', NULL, NULL)。用上面的方法,把ibd转成16进制文件之后打开来看,虽然在物理上是按插入顺序写入的,但是在逻辑上(即行记录的next_record)是按主键递增串起来的。

大概示意如下图所示(实际上null列不会在行里面占据空间)

按主键递增

然后select *的时候,看到记录的顺序是按主键递增的。

mysql> select * from test_not_inc;
+----+------+------+------+
| c1 | c2   | c3   | c4   |
+----+------+------+------+
|  1 | a    | bbbb | cc   |
|  2 | bb   | NULL | NULL |
|  3 | cc   | NULL | dd   |
|  4 | dddd | e    | NULL |
+----+------+------+------+
4 rows in set (0.00 sec)
Show Comments