[转帖]InnoDB Page结构详解

innodb,page,结构,详解 · 浏览次数 : 0

小编点评

**文章简介** 本文对InnoDB Page进行了较为详细的剖析,建议读者好好研究一下对于InnoDB B+数的高度计算并不能光从理论计算,还需要考虑分裂导致的Garbage对于InnoDB Page的研究强烈建议使用文中我提到的两个神器,且建议结合使用生产环境会比我们的实验环境复杂地多,需要各位读者结合生产环境自行研究了。 **主要内容** * InnoDB表聚集索引层什么时候发生变化? * MySQL系列:innodb源码分析之page结构解析 * MySQL是怎样运行的--笔记二 * 我眼中的《庆余年》使用Python分析北京积分落户数据 * 去IOE or Not?拉里·佩奇(Larry Page)的伟大归来《吊打面试官》系列-Redis基础唯一ID生成算法剖析 * 数据库运维能力思考 * 归纳总结 **文章结论** 本文对InnoDB Page进行了较为详细的剖析,建议读者好好研究一下对于InnoDB B+数的高度计算并不能光从理论计算,还需要考虑分裂导致的Garbage对于InnoDB Page的研究强烈建议使用文中我提到的两个神器,且建议结合使用生产环境会比我们的实验环境复杂地多,需要各位读者结合生产环境自行研究了。

正文

1导读

本文花了比较多的时间梳理了InnoDB page的结构以及对应的分裂测试,其中测试部分大部分是参考了叶老师在《InnoDB表聚集索引层什么时候发生变化》一文中使用的方法,其次,本文中的测试工具用到了如下两个工具:

  • innblock:https://github.com/gaopengcarl/innblock

  • innodbruby:https://github.com/jeremycole/innodbruby

本文篇幅较长,强烈建议使用电脑阅读本文。

一、page结构

InnoDB Page结构图

1、File Header结构,记录了page页的一些头信息,一共38字节

innodb存储页类型

2、page header,记录页的状态信息,共56字节

3、虚拟最大最小记录(Infimum and Supremum Records)

最大记录是这个数据页中逻辑上最大的记录,所有用户的记录都小于它。最小记录是数据页上最小的记录,所有用户记录都大于它。他们在数据页被创建的时候创建,而且不能被删除。引入他们主要是方便页内操作。

4、用户记录

用户所有插入的记录都存放在这里,默认情况下记录跟记录之间没有间隙,但是如果重用了已删除记录的空间,就会导致空间碎片。每个记录都有指向下一个记录的指针,但是没有指向上一个记录的指针。记录按照主键顺序排序。即,用户可以从数据页最小记录开始遍历,直到最大的记录,这包括了所有正常的记录和所有被delete-marked记录,但是不会访问到被删除的记录(PAGE_FREE)。

5、Free Space

从PAGEHEAPTOP开始,到最后一个数据目录,这之间的空间就是空闲空间,都被重置为0,当用户需要插入记录时候,首先在被删除的记录的空间中查找,如果没有找到合适的空间,就从这里分配。空间分配给记录后,需要递增PAGENRECS和PAGENHEAP。

6、Page Directory

用户的记录是从低地址向高地址扩展,而数据目录则相反。在数据页被初始化的时候,就会在数据页最后(当然在checksum之前)创建两个数据目录,分别指向最大和最小记录。之后插入新的数据的时候,需要维护这个目录,例如必要的时候增加目录的个数。每个数据目录占用两个字节,存储对应记录的页内偏移量。假设目录N,这个目录N管理目录N-1(不包括)和目录N之间的记录,我们称目录N own 这些记录。在目录N指向的记录中,会有字段记录own记录的数量。由此可见,目录own的记录不能太多,因为太多的话,即意味着目录太过稀疏,不能很好的提高查询效率,但同时也不能own太少,这会导致目录数量变多,占用过多的空间。在InnoDB的实现中,目录own的记录数量在4-8之间,包括4和8,平均是6个记录。如果超过这个数量,就需要重新均衡目录的数量。目录的增加和删除可能需要进行内存拷贝,但是由于目录占用的总体空间很小,开销可以忽略不计。

7、File Trailer

这个部分处于数据页最后的位置,只有8个字节。低地址的四个字节存储checksum的值,高地址的四个字节存储FILPAGELSN的低位四字节。注意这里的checksum的值不一定与FILPAGESPACEORCHKSUM的相同,这个依赖不同的checksum计算方法。

找到一张比较不错的page结构图

二、行记录格式

Record Header

从这个图中可以看到,一条记录的Record Header至少为5字节,对Record Header长度取决于边长字段及字段是否可为空 简言之,记住几条关键规则 一条记录的record header,至少是5字节 对record header影响最大的是变长列数量,及其是否允许为NULL的属性

关于变长列
  • 每个变长列,如果列长度 <128 字节,则需要额外1字节

  • 每个变长列,如果列长度 >=128 字节,则需要额外2字节

  • 如果没有任何变长列,则无需这额外的1-2字节

  • 变长类型为char/varchar/text/blob等

  • 同学们可能会诧异,char为什么也当做变长类型了?这是因为,当字符集非latin1时,最大存储长度可能会超过255字节,例如 char(65) utf8mb4 最长就可以存储260字节,此时在record header中需要用2字节来表示其长度,因此也被当做变长类型了

关于列允许为NULL
  • 每个列如果允许为NULL,则增加 1bit,不足8bit也需要额外1字节

  • 例如只有2个列允许为NULL,只需要2bit来表示,但也需要占用1字节

  • P.S,在InnoDB的存储结构里,从tablespace到segment,再到extent、page,还是file层面,总有各种必要的header或trailer信息需要消耗额外的字节数,不像MyISAM那么简单。

三、数据page结构

主键非叶子节点

  • Record Header,根据主键类型不同占用字节数不同,例如一个int主键,Record Header为5字节,另外需要考虑变长类型、是否为空

  • 聚集索引key的长度,比如int为4字节

  • 指向子节点指针,固定4字节

主键叶子节点

  • Record Header,根据主键类型不同占用字节数不同,例如一个int主键,Record Header为5字节,另外需要考虑变长类型、是否为空

  • 聚集索引key的长度,比如int为4字节

  • 事务ID,固定6字节

  • 回滚段指针,固定7字节

  • 其他列占用空间

二级索引非叶子节点

  • Record Header,根据主键类型不同占用字节数不同,例如一个int主键,Record Header为5字节,另外需要考虑变长类型、是否为空

  • 二级索引列长度

  • 主键索引列长度

  • 指向子节点指针,固定4字节

二级索引叶子节点

  • Record Header,根据主键类型不同占用字节数不同,例如一个int主键,Record Header为5字节,另外需要考虑变长类型、是否为空

  • 二级索引key大小

  • 主键索引列长度

四、一个只有1个page的B+树page

  • Index ID:索引ID,可在informationschema下的INNODBSYS_INDEXES表查到

  • Level:索引高度

  • Prev:上一个page指针

  • Next:下一个page指针

  • N_Heap:表内所有记录数(包含虚拟最大最小记录)

  • N_Recs:表内用户记录数

  • NDirSlots:slots数量

  • Directory:分别记录每个slot的最后一条数据的便宜量,是一个列表

  • MaxTrxD:page内所有记录的最大事务ID

  • Direction:上一条记录的插入方向

  • N_Direction:同一个方向连续插入记录数

  • Garbage Size:可回收空间

  • Garbage off:可回收空间的最近一条记录的偏移量

五、InnoDB表高度计算

相信这是一个面试经常会被问到的问题,那么以三层高度的B树来说,这也是日常生活中最常碰到树的高度了。对于一棵三层高度的B树计算最多存储记录数非常好计算,无非就是根节点最大存储记录数N1中间节点最大存储记录数N2叶子节点最大存储记录数N3得到N,这个N就是当前数据存储结构下的三层高度B树的最大存储记录数。

那么问题来了,对于这个N1、N2、N3分别应该怎么来计算呢?

N1

  • 根节点单行长度计算公式 page大小(16*1024=16384)- 必要信息(File Header38字节+page header56字节+虚拟最大最小记录26字节+Page Directory4字节+File Trailer8字节)=16252字节

  • 每行需要一个指向下一层page的节点的指针,占用4字节

  • 约每4条记录占用一个slot,一个slot大小占用2字节

  • 单行长度计算公式为:row header5字节 + 主键索引列4字节 + 指针4字节 = 13字节 

那么单个page最多能容纳最多行数为 单行长度N1+N1/4*2 = 16252,N1为1203

N2

中间节点的计算方式等同于根节点

N3

  • 不同于非叶子节点,叶子节点需要保留1/16的空间用做变长更新的保留空间,因此,叶子节点的真实可用空间约为15228字节

  • row header(5字节+可变长+非空占位符)+ 主键key长度(如果没有显示声明默认会创建6字节row id)+ trxid6字节+ rollptr7字节

  • 以一个只有一个int列的主键表来说,这个单行长度就为5+4+6+7=22字节

  • 约每4条记录占用一个slot,一个slot大小占用2字节

单行长度N3+N3/4*2 =15228,N3为676

六、测试及分析过程

  1. 15872 │ │
  2. 15936 │ │
  3. 16000 │ █▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋│
  4. 16064 │█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋│
  5. 16128 │█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋│
  6. 16192 │█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋│
  7. 16256 │█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋│
  8. 16320 │█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋███████▋│
  9. ╰────────────────────────────────────────────────────────────────╯
  10. Legend (█ = 1 byte):
  11. Region Type Bytes Ratio
  12. █ FIL Header 38 0.23%
  13. █ Index Header 36 0.22%
  14. █ File Segment Header 20 0.12%
  15. █ Infimum 13 0.08%
  16. █ Supremum 13 0.08%
  17. █ Record Header 3380 20.63%
  18. █ Record Data 11492 70.14%
  19. █ Page Directory 340 2.08%
  20. █ FIL Trailer 8 0.05%
  21. ░ Garbage 0 0.00%
  22. Free 1044 6.37%

分析如下:

  • 固定开销(38字节+36字节+20字节+13字节+13字节+8字节)

  • 记录头部3380字节(676*5)

  • 记录大小11492字节(676*17)

  • 共170个slot(2*170占340字节)

  • free space大小为1044字节(保留1024字节+20空闲)

此时B树高度为1

  1. [root@izbp13wpxafsmeraypddyvz data]# innodb_space -s ibdata1 -T xucl/t1 -p 3 space-indexes
  2. id name root fseg fseg_id used allocated fill_factor
  3. 2063 PRIMARY 3 internal 1 1 1 100.00%
  4. 2063 PRIMARY 3 leaf 2 0 0 0.00%

再次插入一条数据,这个时候树的结构就发生变化了

  1. [root@izbp13wpxafsmeraypddyvz data]# innodb_space -s ibdata1 -T xucl/t1 -p 3 space-indexes
  2. id name root fseg fseg_id used allocated fill_factor
  3. 2063 PRIMARY 3 internal 1 1 1 100.00%
  4. 2063 PRIMARY 3 leaf 2 2 2 100.00%

可以看到,这个时候树的高度就变成了二层,page no=3为根节点,page no=4和page no=5是它的叶子节点

  1. [root@izbp13wpxafsmeraypddyvz data]# innodb_space -s ibdata1 -T xucl/t1 space-summary
  2. page type prev next lsn
  3. 0 FSP_HDR 0 0 70484631053
  4. 1 IBUF_BITMAP 0 0 70484571536
  5. 2 INODE 0 0 70484631053
  6. 3 INDEX 0 0 70484631053
  7. 4 INDEX 0 5 70484631053
  8. 5 INDEX 4 0 70484631053

我们仍然扫描page no=3这个page

  1. 16256 │ │
  2. 16320 │ █▋█▋█████▋│
  3. ╰────────────────────────────────────────────────────────────────╯
  4. Legend (█ = 1 byte):
  5. Region Type Bytes Ratio
  6. █ FIL Header 38 0.23%
  7. █ Index Header 36 0.22%
  8. █ File Segment Header 20 0.12%
  9. █ Infimum 13 0.08%
  10. █ Supremum 13 0.08%
  11. █ Record Header 10 0.06%
  12. █ Record Data 16 0.10%
  13. █ Page Directory 4 0.02%
  14. █ FIL Trailer 8 0.05%
  15. ░ Garbage 0 0.00%
  16. Free 16226 99.04%

可以看到这个page含有2条记录,每条记录record header5字节,数据部分8字节(id列4字节+4字节指针) 扫描page no=4这个page

  1. Legend (█ = 1 byte):
  2. Region Type Bytes Ratio
  3. █ FIL Header 38 0.23%
  4. █ Index Header 36 0.22%
  5. █ File Segment Header 20 0.12%
  6. █ Infimum 13 0.08%
  7. █ Supremum 13 0.08%
  8. █ Record Header 1690 10.31%
  9. █ Record Data 5746 35.07%
  10. █ Page Directory 172 1.05%
  11. █ FIL Trailer 8 0.05%
  12. ░ Garbage 7436 45.39%
  13. Free 1212 7.40%

从统计信息可以分析出,这个page共338条数据,Garbage size为7436(注意一下,这个后面会有用到)

  1. Legend (█ = 1 byte):
  2. Region Type Bytes Ratio
  3. █ FIL Header 38 0.23%
  4. █ Index Header 36 0.22%
  5. █ File Segment Header 20 0.12%
  6. █ Infimum 13 0.08%
  7. █ Supremum 13 0.08%
  8. █ Record Header 1695 10.35%
  9. █ Record Data 5763 35.17%
  10. █ Page Directory 170 1.04%
  11. █ FIL Trailer 8 0.05%
  12. ░ Garbage 0 0.00%
  13. Free 8628 52.66%

从统计信息可以分析出,这个page共339条数据,两个page加起来共677条数据,也符合我们的结果。

到这里,二层分裂我们已经测试完了,我们继续往下测试三层高度,按照我们先前的理论基础,2层高度最多可以容纳约813228条数据(实际上并不是这个值,至于原因我后面再讲),我们利用以下脚本构造t1(我把t1表truncate了),我插入了812890条数据

  1. #!/bin/env python
  2. #coding:utf-8
  3. import pymysql
  4. def insert_mysql():
  5. conn = pymysql.connect(host='127.0.0.1', user='xucl',
  6. password='xuclxucl123', database='xucl', charset='utf8')
  7. cursor = conn.cursor()
  8. for i in range(812890):
  9. sql = "insert into t1 values({});".format(i)
  10. cursor.execute(sql)
  11. conn.commit()
  12. conn.close()
  13. if __name__ == "__main__":
  14. insert_mysql()

此时树高度为2层

  1. [root@izbp13wpxafsmeraypddyvz data]# innblock xucl/t1.ibd scan 16
  2. ----------------------------------------------------------------------------------------------------
  3. Welcome to use this block analyze tool:
  4. [Author]:gaopeng [Blog]:blog.itpub.net/7728585/abstract/1/ [QQ]:22389860
  5. [Review]:yejinrong@zhishutang [Blog]:imysql.com [QQ]:4700963
  6. ----------------------------------------------------------------------------------------------------
  7. Datafile Total Size:28311552
  8. ===INDEX_ID:2063
  9. level1 total block is (1)
  10. block_no: 3,level: 1|*|
  11. level0 total block is (1203)

根节点存储了1203条数据

我用如下方法扫描并统计了每个叶子page内的数据量

  1. [root@izbp13wpxafsmeraypddyvz data]# cat test.sh
  2. #!/bin/bash
  3. for i in {3..1235};
  4. do
  5. innblock xucl/t1.ibd ${i} 16|grep n_rows
  6. done

扫描结果如下:

  1. [root@izbp13wpxafsmeraypddyvz data]# sh test.sh
  2. slot_nums:301 heaps_rows:1205 n_rows:1203
  3. slot_nums:86 heaps_rows:678 n_rows:338
  4. slot_nums:170 heaps_rows:678 n_rows:676
  5. slot_nums:170 heaps_rows:678 n_rows:676
  6. slot_nums:170 heaps_rows:678 n_rows:676
  7. slot_nums:170 heaps_rows:678 n_rows:676
  8. slot_nums:170 heaps_rows:678 n_rows:676
  9. slot_nums:170 heaps_rows:678 n_rows:676
  10. slot_nums:170 heaps_rows:678 n_rows:676
  11. slot_nums:170 heaps_rows:678 n_rows:676
  12. slot_nums:170 heaps_rows:678 n_rows:676
  13. ...

我们注意到page no=4这个page,只存储了338条记录,我们用innodb_space扫描这个page

  1. Legend (█ = 1 byte):
  2. Region Type Bytes Ratio
  3. █ FIL Header 38 0.23%
  4. █ Index Header 36 0.22%
  5. █ File Segment Header 20 0.12%
  6. █ Infimum 13 0.08%
  7. █ Supremum 13 0.08%
  8. █ Record Header 1690 10.31%
  9. █ Record Data 5746 35.07%
  10. █ Page Directory 172 1.05%
  11. █ FIL Trailer 8 0.05%
  12. ░ Garbage 7436 45.39%
  13. Free 1212 7.40%

很清楚地可以看到,Garbage size为7436,恰好为338条记录大小,至于原因我个人的猜测是因为在一层变二层高度时,由原先page内数据逐条删除,插入新的page,删除的空间变成了Garbage,新page由于是新插入的,所以不存在Garbage(以上未经求证),这里比较细节。

这样一算,插入812890条数据以后,所有的page都已经占满,这也就解释了最大记录数不是理论值813228了。接着我们再插入一条数据

  1. root@mysqldb 20:12: [xucl]> select count(*) from t1;
  2. +---------+
  3. | max(id) |
  4. +---------+
  5. | 812890 |
  6. +---------+
  7. 1 row in set (0.00 sec)
  8. root@mysqldb 20:21: [xucl]> insert into t1 values(812890);
  9. Query OK, 1 row affected (0.01 sec)

B树变成了三层高度

  1. [Review]:yejinrong@zhishutang [Blog]:imysql.com [QQ]:4700963
  2. ----------------------------------------------------------------------------------------------------
  3. Datafile Total Size:28311552
  4. ===INDEX_ID:2063
  5. level2 total block is (1)
  6. block_no: 3,level: 2|*|
  7. level1 total block is (2)
  8. block_no: 36,level: 1|*|block_no: 37,level: 1|*|
  9. level0 total block is (1204)

七、遗留问题

  • 关于heaptop,一直没有搞懂计算方法,根据我的多次观察,heaptop的便宜量总是最后一条记录的位置点-5,咱不清楚为什么

  • 关于page no=4 garbage的问题没有深入研究

八、结论

  • 本文对InnoDB Page有了较为详细的剖析,建议读者好好研究一下

  • 对于InnoDB B+数的高度计算并不能光从理论计算,还需要考虑分裂导致的Garbage

  • 对于InnoDB Page的研究强烈建议使用文中我提到的两个神器,且建议结合使用

  • 生产环境会比我们的实验环境复杂地多,需要各位读者结合生产环境自行研究了

参考资料:《InnoDB表聚集索引层什么时候发生变化》: https://zhishutang.com/vca 《MySQL系列:innodb源码分析之page结构解析》 :https://www.2cto.com/database/201412/365376.html 《MySQL是怎样运行的--笔记二》: https://cs704.cn/?p=352

QQ群号:763628645

QQ群二维码如下, 添加请注明:姓名+地区+职位,否则不予通过

订阅我的微信公众号“杨建荣的学习笔记”,第一时间免费收到文章更新。别忘了加星标,以免错过新推送提示。

7

   

近期热文

你可能也会对以下话题感兴趣。点击链接就可以查看。

8

   

转载热文

你可能也会对以下话题感兴趣,文章来源于转载,点击链接就可以查看。

与[转帖]InnoDB Page结构详解相似的内容:

[转帖]InnoDB Page结构详解

1导读 本文花了比较多的时间梳理了InnoDB page的结构以及对应的分裂测试,其中测试部分大部分是参考了叶老师在《InnoDB表聚集索引层什么时候发生变化》一文中使用的方法,其次,本文中的测试工具用到了如下两个工具: innblock:https://github.com/gaopengcarl

【转帖】MySQL InnoDB存储原理深入剖析与技术分析

一、MySQL记录存储: MySQL InnoDB的数据由B+树来组织,数据记录存储在B+树数据页(page)中,每个数据页16kb,数据页 包括页头、虚记录、记录堆、自由空间链表、未分配空间、slot区、页尾七部分组成。 所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)

[转帖]py_innodb_page_info.py工具使用

目录 一、Linux安装Python3 1. 解压包 2. 安装环境 3. 生成编译脚本 4. 检查python3.10的编译器 5. 建立Python3和pip3的软链 6. 添加到PATH 7. 验证Python3和pip3是否正常 二、py_innodb_page_info.py工具 1. p

[转帖]Innodb存储引擎-idb文件格式解析

文章目录 ibd 文件格式解析idb文件page类型和格式(File Header & Trailer)FIL_PAGE_TYPE_FSP_HDR格式Extent Descriptor格式Extent Descriptor链表管理Inode page链表管理 FIL_PAGE_INODE格式Segm

[转帖]InnoDB引擎之-两次写(Double Write)

https://www.jianshu.com/p/63f2985fb427 InnoDB引擎有几个重点特性,为其带来了更好的性能和可靠性: 插入缓冲(Insert Buffer) 两次写(Double Write) 自适应哈希索引(Adaptive Hash Index) 异步IO(Async I

[转帖]innodb表最大列数限制

https://blog.csdn.net/sun_ashe/article/details/52433812 innodb 最大列数限制为1023,其中包含3个内部隐藏列,分别为:DB_ROW_ID(如果没有主键的情况), DB_TRX_ID事务id列,DB_ROLL_PTR回滚指针列。 但是分配

[转帖]Innodb存储引擎-备份和恢复(分类、冷备、热备、逻辑备份、二进制日志备份和恢复、快照备份、复制)

文章目录 备份和恢复分类冷备热备逻辑备份mysqldumpSELECT...INTO OUTFILE恢复 二进制日志备份与恢复快照备份(完全备份)复制快照+复制的备份架构 备份和恢复 分类 (1)根据备份的方法可以分为: Hot Backup(热备):指在数据库运行中直接备份,对正在运行的数据库没有

[转帖]Innodb存储引擎-锁(数据库锁的查看、快照读&当前读、MVCC、自增长与锁、外键与锁、行锁、并发事务的问题、阻塞、死锁、锁升级、锁的实现)

文章目录 锁lock 与latch读锁/写锁/意向锁INNODB_TRX/INNODB_LOCKS/INNODB_LOCK_WAITS一致性非锁定读(快照读)一致性锁定读(当前读)MVCC版本链Read View流程 自增长与锁外键和锁行锁类型记录锁(record lock)间隙锁(gap lock

[转帖]InnoDB表聚集索引层高什么时候发生变化

导读 本文略长,主要解决以下几个疑问 1、聚集索引里都存储了什么宝贝 2、什么时候索引层高会发生变化 3、预留的1/16空闲空间做什么用的 4、记录被删除后的空间能回收重复利用吗 1、背景信息 1.1 关于innodb_fill_factor 有个选项 innodb_fill_factor 用于定义

[转帖]MySQL InnoDB存储引擎大观

https://baijiahao.baidu.com/s?id=1709263187856706948&wfr=spider&for=pc MySQL InnoDB 引擎现在广为使用,它提供了事务,行锁,日志等一系列特性,本文分析下 InnoDB的内部实现机制,MySQL 版本为 5.7.24,操