【innodb阅读笔记】之 行格式(Dynamic)

发布于:2024-12-18 ⋅ 阅读:(62) ⋅ 点赞:(0)

一、背景

        Innodb 1.0 版本开始引入了新的行格式 dynamic,新的行格式在存放 blob 中的数据采用了完全行溢出的方式,在数据页中只存放 20 字节的指针,实际数据都存放在 Off page 中,而 compact 会存放 768 个前缀字节。

二、dynamic 行记录格式

        数据区在数据⾏中的位置如下图所⽰:

        从图中可以观察到,dynamic 行记录格式的首部是一个非 null 变长字段长度列表,并且是按照列的逆序放置的,其长度为:

        若列的长度小于255字节,用 1 字节表示

        若大于255个字节,则用 2 字节表示

        边长字段的长度最大不能超过两个字节,这是因为 mysql 数据库中 varchar 类型的最大长度限制为 65535。

        变长字段之后的第二个部分是 null 标识位,该标识指示了该行数据是否有 null 值,有则用 1 表示,该部分所占字节应该为 1 字节。如果创建表结构数据列都指定为非空,则 null 标识位省略。

        接下来的部分是记录头信息,固定占用 5 字节,每位含义如下:

Dynamic 记录头信息
名称 大小(bit) 描述
() 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 表示 最小记录,3 表示 最大记录。
next_record 16 页中 下一条记录的相对位置
total 40  合计

从分隔线向右第⼀个字段存储真实数据的主键值,对于主键值有以下⼏种情况:

        如果表中定义了主键,则直接存储主键的值;

        如果是复合主键会根据列定义的顺序依次排列在这⾥;

        如果没有主键,会优先使⽤第⼀个不允许为NULL的 UNIQUE 唯⼀列作为主键;

        如果既没有主键也没有唯⼀键,那么InnoDB会构建⼀个6字节的字段 DB_ROW_ID 作为⾏的唯⼀标识,存储在真实数据的头部 

紧接着是在事务运⾏中两个⾮常重要的固定字段,

        6 字节的事务ID字段 DB_TX_ID ,记录创建或最后⼀次修改该记录的事务ID

        7 字节的回滚指针字段 DB_ROLL_PTR ,如果在事务中这条记录被修改,指向这条记录的上⼀个版本

        接下来就是除了主键和值为NULL的列之外,其他列的真实数据,按照顺序从左到右依次排列

        ⾄于为什么不存储 NULL 值,原因很简单,就是为了节少空间,所有允许为 NULL 的列都会在⾏额外信息区的 NULL 值列表中进⾏标识。

三、实践

# 创建表结构
mysql> create table mytest (
    ->  t1 varchar(10),
    ->  t2 varchar(10),
    ->  t3 char(10),
    ->  t4 VARCHAR(10)
    -> ) engine=innodb charset=latin1 row_format = dynamic;
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条数据

四、验证 null 标识位

# 创建表结构
create table mytest1 (
    t1 varchar(10) not null
) engine=innodb charset=latin1 row_format = dynamic;

# 插入数据
mysql> insert into mytest1 select 'aaa';
Query OK, 1 row affected (0.01 sec)

# 用 notepad++ 打开 mytest1.ibd 文件,需要下载 hex-editor 插件, 定位到C078位置
03                            # 变长字段列表 逆序
00 00 10 ff f2                # record header 
00 00 00 00 04 0a             # rowid
00 00 00 00 0b 15             # transactionid
b2 00 00 01 26 01 10          # roll pointer
61 61 61                      # 第一列数据
# 整理完成后,我们发现, 当一行数据都是非空字段时,null 标识位是省略的

五、验证行溢出

# 创建表结构
mysql> create table mytest1 (
    ->   t1 varchar(53353) not null
    -> ) engine=innodb charset=latin1 row_format = dynamic;
Query OK, 0 rows affected (0.02 sec)

# 插入数据
mysql> insert into mytest1  select repeat('a', 53353);
Query OK, 1 row affected (0.01 sec)


# 用 notepad++ 打开 mytest1.ibd 文件,需要下载 hex-editor 插件, 定位到C078位置
14 c0                # 变长字段标识
00 00 10 ff f1       # header 
00 00 00 00 04 0b
00 00 00 00 0b 1b
b6 00 00 01 2a 01 10

# 使⽤ 20 个字节来标记这个溢出⻚的位置信息
00 00 00 31 
00 00 00 04 
00 00 00 26 
00 00 00 00 
00 00 d0 69 # 指针

# 使用 py_innodb_page_info.py 查看 mytest1.ibd 文件
D:\ProgramData\MySQL\py_innodb_page_type>py_innodb_page_info.py -v "D:/ProgramData/MySQL/MySQL Server 5.7/MySQL_data/innodb_test/mytest1.ibd"
page offset 00000000, page type <File Space Header>
page offset 00000001, page type <Insert Buffer Bitmap>
page offset 00000002, page type <File Segment inode>
page offset 00000003, page type <B-tree Node>, page level <0000>
page offset 00000004, page type <Uncompressed BLOB Page>
page offset 00000005, page type <Uncompressed BLOB Page>
page offset 00000006, page type <Uncompressed BLOB Page>
page offset 00000007, page type <Uncompressed BLOB Page>

# 我们发现,存在一个数据页,存在 4 个溢出页,同时通过上面的验证,
# 我们知道数据行中存储的是 20 位的指针数据


六、实践 min_rec_flag 标识位

# 创建表结构 
create table mytest1 (
   t1 varchar(8090)
) engine=innodb charset=latin1 row_format = dynamic;

# 插入数据
mysql> insert into mytest1  select repeat('a', 8090);
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> insert into mytest1  select repeat('b', 8090);
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> insert into mytest1  select repeat('c', 8090);
Query OK, 1 row affected (0.01 sec)
Records: 1  Duplicates: 0  Warnings: 0

# 查看表空间情况
D:\ProgramData\MySQL\py_innodb_page_type>py_innodb_page_info.py -v "D:/ProgramData/MySQL/MySQL Server 5.7/MySQL_data/innodb_test/mytest1.ibd"
page offset 00000000, page type <File Space Header>
page offset 00000001, page type <Insert Buffer Bitmap>
page offset 00000002, page type <File Segment inode>
page offset 00000003, page type <B-tree Node>, page level <0001>
page offset 00000004, page type <B-tree Node>, page level <0000>
page offset 00000005, page type <B-tree Node>, page level <0000>
Total number of page: 6:
Insert Buffer Bitmap: 1
File Space Header: 1
B-tree Node: 3
File Segment inode: 1

# 我们发现存在非叶子节点, 解析第一个非页子节点 
00             # 变长字段列表
10 00 11 00 10 # record header 
00 00 00 00 04 0c # 子目录最小值 主键
00 00 00 04 # 对应子目录位置

# 对于第一行 00 表示为变长字段列表 对于非子节点 默认为 00

# 第二行为 record header 占用 5 字节
#    第1个字节转换为二进制 0 0 0 1 0 0 0 0
#        预留字段 0 
#        预留字段 0
#        deleted_flag 0 该行 未删除
#        min_rec_flag 1 当前记录 为索引节点,同时为 最小记录,所以值为 1
#        n_owned      0000  该组拥有的记录数 不记录在当前节点
#    第 2 个和第 3 个字节转为二进制 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1
#        heap_no 0 0 0 0 0 0 0 0 0 0 0 1 0 排在第一行数据
#        record_type 0 0 1 表示索引数据
#    第3个和第5个字节 next_record 0010 下一行的记录为当前位置 + 0010
#        C07d + 0010 = C08d 下一行数据的 next_record 位置

# 第三行 为:数据页最小主键,主键值为 00 00 00 00 04 0c
# 第四行 为:数据页编号,当前记录的叶子节点变化为:00 00 00 04
# 对应 page offset 00000004, page type <B-tree Node>, page level <0000>



网站公告

今日签到

点亮在社区的每一天
去签到