一、Innodb行记录格式
innodb 存储引擎同大多数数据库一样,记录是以行的形式存储的。这意味着页中保存的一行行的数据。在 mysql 5.7 版本中,默认格式为 Dynamic,可以通过命令查看当前表的行格式,其中 row_format 表示当前表行记录格式类型:
# 查询 数据表 z 的行格式 默认为 dynamic
mysql> show table status like 'z' \G;
*************************** 1. row ***************************
Name: z
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 3
Avg_row_length: 5461
Data_length: 16384
Max_data_length: 0
Index_length: 16384
Data_free: 0
Auto_increment: NULL
Create_time: 2024-12-05 10:19:54
Update_time: 2024-12-05 10:25:48
Check_time: NULL
Collation: utf8mb4_general_ci
Checksum: NULL
Create_options:
Comment:
1 row in set (0.00 sec)
二、Compact 行记录格式
compact 行记录格式是在 mysql 5.0 中引入的,其设计目的是高效的存储数据,简单来说,一个页中存放的行数据越多,其性能就越高。图中显示了 compact 行记录的存储方式。
从图中可以观察到,compact 行记录格式的首部是一个非 null 变长字段长度列表,并且是按照列的逆序放置的,其长度为:
若列的长度小于255字节,用 1 字节表示
若大于255个字节,则用两字节表示
边长字段的长度最大不能超过两个字节,这是因为 mysql 数据库中 varchar 类型的最大长度限制为 65535。
变长字段之后的第二个部分是 null 标识位,该标识指示了该行数据是否有 null 值,有则用 1 表示,该部分所占字节应该为1字节。
接下来的部分是记录头信息,固定占用 5 字节,每位含义如下:
名称 | 大小 | 描述 |
---|---|---|
() | 1 | 未知 |
() | 1 | 未知 |
deleted_flag | 1 | 该行是否已被删除 |
min_rec_flag | 1 | 存储目录项记录中主键值最小的目录项记录置为1,其它情况都置0. |
n_owned | 4 | 页目录中每个组的最后一条记录会存储该组的记录数,作为n_owned 字段。值的关注的是,在mysql中最小记录是一组,普通记录与其它记录是一组,因此最小记录中n_owned属性是1,最大记录的n_owned值是5. |
heap_no | 13 | 当前页中该记录的排序位置 |
record_type | 3 | 记录类型 0 表示普通类型,1表示B+树的非叶子节点 2,2表示最小记录,3表示最大记录。 |
next_record | 16 | 页中 下一条记录的相对位置 |
total | 40 | 合计 |
最后部分是实际存储每个列的数据,需要注意的是 null 不占该部分任何空间,每行除了用户定义的列外,还有两个隐藏列,事务id列,回滚指针列,分别为 6 字节和 7 字节大小,若没有定义主键列,还会增加一个 6 字节大小的 rowid 列。
接下来用一个具体示例来分析 compact 行记录的内部格式:
# 创建表结构
mysql> create table mytest (
-> t1 varchar(10),
-> t2 varchar(10),
-> t3 char(10),
-> t4 VARCHAR(10)
-> ) engine=innodb charset=latin1 row_format=compact;
Query OK, 0 rows affected (0.03 sec)
# 插入数据
mysql> INSERT INTO mytest VALUES ('a', 'bb', 'bb', 'ccc');
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO mytest VALUES ('d', 'ee', 'ee', 'fff');
Query OK, 1 row affected (0.00 sec)
mysql> insert into mytest values ('d', null, null, 'fff');
Query OK, 1 row affected (0.00 sec)
# 用 notepad++ 打开 mytest.ibd 文件,需要下载 hex-editor 插件, 定位到C078位置
# 第一行数据
03 02 01 # 变长字段列表 逆序
00 # null 标识位
00 00 10 00 2c # record header
00 00 00 00 02 01 # rowid
00 00 00 00 05 56 # transactionid
d1 00 00 01 50 01 10 # roll pointer
61 # 第一列数据
62 62 # 第二列数据
62 62 20 20 20 20 20 20 20 20 # 第三列数据
63 63 63 # 第四列数据
# 第一行 变长字段列表为逆序状态,转换回来为01 02 03,
# 对应第一列、第二列、第4列长度分别为、1字节、2字节、3字节
# 第二行 null 标识位 目前没有null的列 所以为 00
# 第三行为 record header 占用5字节
# 第1个字节转换为二进制 0 0 0 0 0 0 0 0
# 未知 0
# 未知 0
# deleted_flag 0 改行未删除
# min_rec_flag 0 该行不是最小记录
# n_owned 0000 该组拥有的记录数 不记录在当前节点
# 第2个和第3个字节转为二进制 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
# heap_no 0 0 0 0 0 0 0 0 0 0 0 1 0 排在第一行数据
# record_type 0 0 0 表示普通记录
# 第3个和第5个字节 next_record 002C 下一行的记录为当前位置 + 002C
# C080 + 002C = C0AC 下一行数据的next_record位置
# 第二行数据
03 02 01 # 边长字段列表 逆序
00 # null 标识位
00 00 18 00 2b # record header
00 00 00 00 02 02 # rowid
00 00 00 00 05 57 # transactionid
d2 00 00 01 51 01 10 # roll pointer
64 # 第一列数据
65 65 # 第二列数据
65 65 20 20 20 20 20 20 20 20 # 第三列数据
66 66 66 # 第四列数据
# 第一行 变长字段列表为逆序状态,转换回来为01 02 03,
# 对应第一列、第二列、第4列长度分别为、1字节、2字节、3字节
# 第二行 null 标识位 目前没有null的列 所以为 00
# 第三行为 record header 占用5字节
# 第1个字节转换为二进制 0 0 0 0 0 0 0 0
# 未知 0
# 未知 0
# deleted_flag 0 改行未删除
# min_rec_flag 0 该行不是最小记录
# n_owned 0000 该组拥有的记录数 不记录在当前节点
# 第2个和第3个字节转为二进制 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0
# heap_no 0 0 0 0 0 0 0 0 0 0 0 1 1 排在第二行数据
# record_type 0 0 0 表示普通记录
# 第3个和第5个字节 next_record 002B 下一行的记录为当前位置 + 002B
# C0AC + 002B = C0D7 下一行数据的next_record位置
# 第三行数据
03 01 # 边长字段列表 逆序
06 # null 标识位
00 00 20 00 1F # record header
00 00 00 00 02 03 # rowid
00 00 00 00 05 58 # transactionid
d3 00 00 01 52 01 10 # roll pointer
64 # 第一列数据
66 66 66 # 第四列数据
# 第一行 变长字段列表为逆序状态,转换回来为01 03,
# 对应第1列、第4列长度分别为1字节、3字节
# 第二行 null 标识位 06 转换为二进制 0110
# 第二列、第三列 为 null
# 第三行为 record header 占用5字节
# 第1个字节转换为二进制 0 0 0 0 0 0 0 0
# 未知 0
# 未知 0
# deleted_flag 0 改行未删除
# min_rec_flag 0 该行不是最小记录
# n_owned 0000 该组拥有的记录数 不记录在当前节点
# 第2个和第3个字节转为二进制 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
# heap_no 0 0 0 0 0 0 0 0 0 0 1 0 0 排在第 3 行数据
# record_type 0 0 0 表示普通记录
# 第3个和第5个字节 next_record 001F 下一行的记录为当前位置 + 001F
# C0D7 + 001F = C0F6 下一行数据的next_record位置
# 当我们在插入 5 条数据
mysql> insert into mytest values ('d', null, null, 'fff');
Query OK, 1 row affected (0.00 sec)
mysql> insert into mytest values ('e', null, null, 'ggg');
Query OK, 1 row affected (0.00 sec)
mysql> insert into mytest values ('h', null, null, 'lll');
Query OK, 1 row affected (0.00 sec)
mysql> insert into mytest values ('m', null, null, 'nnn');
Query OK, 1 row affected (0.01 sec)
mysql> insert into mytest values ('o', null, null, 'ppp');
Query OK, 1 row affected (0.00 sec)
# 解析一下第 4 条数据
03 01 # 边长字段列表 逆序
06 # null 标识位
04 00 28 00 1F # record header
00 00 00 00 02 04 # rowid
00 00 00 00 05 59 # transactionid
d4 00 00 01 53 01 10 # roll pointer
64 # 第一列数据
66 66 66 # 第四列数据
# 重点看一下 record header 第一个字节 04 其中 n_owned 占用4位等于4
# 表明当前数据为当前组的最后一条数据,其中这个组包含4条数据
三