文章目录
- 2. MySQL存储结构
-
- 2.6 当表中的数据很少时如何避免空间浪费?
- 2.7 如果访问的数据跨区了怎么办?区组
-
- 2.7.1 新问题
- 2.8 以上这些数据结构还有优化的空间吗?段
-
- 2.8.1 上面讲的所有操作是在哪里进行的?
- 2.8.2 查询数据时MySQL会一次把表空间中的数据全部加载到内存吗?
- 2.8.3 每查询一条数据都要进行一次磁盘I/O吗?
- 3. 页结构
-
- 3.1 页的大小可以设置吗?
- 3.2 页都有哪些分类?我们需要重点学习哪种页?
-
- 3.2.1 新问题
- 3.3 页头和页尾具体包含了哪些信息?
-
- 3.3.1 什么是LSN?
- 3.3.2 除了页头和页尾,数据页中还有哪些信息?
- 3.3.3 页主体中包含哪些信息?
- 3.4 数据行有哪些信息组成?
-
- 3.4.1 数据行是如何组织在一起的?
- 3.4.2 怎么标识新页中的第一行和最后一行?
- 3.4.3 当向一个新页插入数据时是如何执行的?
- 3.5 如果要查询的数据在某一个页中,如何定位它在页中的位置,一条条遍历吗?
-
- 3.5.1 一条条遍历的查询效率高不高?
- 3.5.2 如何提高页内的查询效率?页目录
- 3.6 关于事务、索引这些信息在页中怎么记录?
- 3.7 数据页的完整结构是什么样的?
2. MySQL存储结构
2.6 当表中的数据很少时如何避免空间浪费?
- 当创建表时,并不知道当前表的数据量级
- 为了节省空间,最初只创建7个初始页(在MySQL5.7中创建6个初始页),而不是一个完整的区,可以通过以下SQL查看:
mysql> select * FROM information_schema.INNODB_TABLESPACES WHERE name = 'test_db/student'\\G
*************************** 1. row ***************************
SPACE: 228
NAME: test_db/student
FLAG: 16417 # 页大小
ROW_FORMAT: Dynamic
PAGE_SIZE: 16384
ZIP_PAGE_SIZE: 0
SPACE_TYPE: Single
FS_BLOCK_SIZE: 4096
FILE_SIZE: 114688 # 数据文件初始大小
ALLOCATED_SIZE: 114688
AUTOEXTEND_SIZE: 0
SERVER_VERSION: 8.0.43
SPACE_VERSION: 1
ENCRYPTION: N
STATE: normal
1 row in set (0.01 sec)
# 根据数据文件大小和每页大小计算出页数
# 114688 / 16384 = 7 个数据页
mysql>
- 这些零散页会放在表空间中一个叫碎片区的区域,随着数据量的增加,会申请新的页来存储数据,32页之前是随机分布在碎片区,当碎片区达到32个页的时候,后续每次都会申请一个完整的区来存储更多的数据;
- 这些“碎片区”本身是集中管理的。虽然页在物理上不连续,但它们逻辑上是在一起的。
- 当表很小(<32页)时,它通常只访问这些碎片区。磁盘磁头在一个相对集中的区域内跳跃,而不是满盘乱飞。
总结:
通过零散页和碎片区避免空间浪费的问题
2.7 如果访问的数据跨区了怎么办?区组
- 当表中的数据越来越多,为了有效的管理区,定义了区组的结构,每个区组固定管理256个区即256MB,通过区组可以在物理结构层面非常高效的管理和定位到每个区

- 第一个区组中的首个区的前四页比特殊,也就是初始页中的前4页,分别是:
- File Space Header: 表空间和区组中条目信息
- Insert Buffer Bitmap:Change Buffer(变更缓冲区)相关信息
- File Segment inode: 段信息
- B-tree Node:索引根信息
- 其他为空闲页用来存储真实的数据
- 其他区组中首个区的结构都一样,前两个页分别是:
- Extent Descriptor(XDES):区组条目信息
- Insert Buffer Bitmap:Change Buffer相关信息
总结:
使用区组结构有效的管理区,每个区组固定管理256个区即 256MB ,区组条目信息中会记录每个区的偏移并用双向链表连接。
2.7.1 新问题
如何定位要查询的目标页?
2.8 以上这些数据结构还有优化的空间吗?段
- “段"并不对应表空间中的连续的物理区域,可以看做是 “区” 和 “页” 的一个附加标注信息,段的主要作用是区分不同功能的区和在碎片区中的页,主要分为"叶子节点段"和"非叶子节点段"等,这两个段和我们常说的B+树索引中的叶子、非叶子节点对应,可以简单的理解为"非叶子节点段” 存储和管理索引树,"叶子节点段"存储和管理实际数据,从逻辑上讲,最终由 “叶子节点段” 和 “非叶子节点段” 等段构成了表空间 .ibd 文件,如下图所示:

总结:
有的,InnoDB使用"段"这个逻辑结构区分不同功能的区和在碎片区中的页,并按功能分为"叶子节点段"和"非叶子节点段",做为B+树索引中的叶子、非叶子节点,从而进一步提升查询效率。
2.8.1 上面讲的所有操作是在哪里进行的?
- 所有的数据库操作都是在内存中进行的,最终会把修改结果刷回磁盘中对应的页中。
2.8.2 查询数据时MySQL会一次把表空间中的数据全部加载到内存吗?
当然不是,使用InnoDB存储引擎创建表,在查询数据时会根据表空间内部定义的数据结构(一般为索引),定位到目标数据行所在的页,只把符合查询要求的页加载到内存。
2.8.3 每查询一条数据都要进行一次磁盘I/O吗?
不一定,每次查询都会把磁盘中数据行对应的数据页加载到内存中,如果当前查询的数据行已经在内存中,则直接从内存中返回结果,从而提高查询效率。
小结:
3. 页结构
页在MySQL运行的过程中起到了非常重要的作用,为了能发挥更好的性能,可以结合自己系统的业务场景和数据大小,对页相关的系统变量进行调整,页的大小就是一个非常重要的调整项。同时关于页的结构也要有所了解,以后介绍的索引原理也是基于页实现的。首先来看关于页的几个问题。
3.1 页的大小可以设置吗?
- 前面介绍了每个数据页默认为 16KB ,是操作系统"数据块" 4KB的整数倍,那么只要保证页的大小是操作系统"数据块" 大小的整数倍是不是也可以呢,答案是肯定的。
- MySQL提供了一个专门的系统变量来控制页的大小,可以通过系统变量 innodb_page_size 进行调整与查看,在调整页大小的时候需要保证设置的值是操作系统"数据块" 4KB的整数倍,从而保证通过操作系统和磁盘交互时"数据块"的完整性,不被分割或浪费,所以规定了innodb_page_size 可以设置的值,分别是 4096 、 8192 、 16384 、 32768 、65536 ,对应 4KB 、 8KB 、 16KB 、 32KB 、 64KB 。
总结:
可以通过系统变量 innodb_page_size 进行调整与查看,但要保证设置的值是操作系统"数据块" 4KB的整数倍,MySQL规定 innodb_page_size 可以设置的值,分别是 4096 、8192 、 16384 、 32768 、 65536 ,对应 4KB 、 8KB 、 16KB 、 32KB 、 64KB 。
3.2 页都有哪些分类?我们需要重点学习哪种页?
- InnoDB在不同的使用场景定义多种不同类型的页,常用的有 数据页 、 Undo Log页 、Change Buffer页 、 Extent Descriptor(XDES)页 、 InnoDB段信息页 等,每种页的数据结构都不相同,其中最需要我们关注的就是数据页,由于InnoDB中有个概念叫 “索引即数据”,所以也叫做索引页。
- 不论哪种类型的页都具有页头(File Header)和页尾(File Trailer)两个信息
3.2.1 新问题
什么是索引页?
3.3 页头和页尾具体包含了哪些信息?
页头和页尾中包含的是用来描述文件相关的信息,如下图所示:

页头 – File Header
-
页号: FIL_PAGE_OFFSET 占用 4Byte ,相当于页的身份证号,通过这个长度可以计算出每个InnoDB表中最多可以拥有 2^(4*8)-1约42亿 个页,表空间第一个页编号从0开始,之后的页号分别是1,2,3…依此类推,具体页的偏移量计算公式为:页号 * 每页大小;那么按照每个页默认16KB大小计算,一个表空间最大容量为 2^(4*8) * 16KB = 64TB ,这也是InnoDB表空间最大容量是64T的原因;
-
上一页 号: FIL_PAGE_PREV
-
下一页 号: FIL_PAGE_NEXT 多个页通过这两个信息组成双向链表,即使不同的页地址不连续,也可以通过链表连接
-
表空间ID: FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID ,当前页属于哪个表空间
-
页类型: FIL_PAGE_TYPE ,数据页对应的页类型是 FIL_PAGE_INDEX = 0x45BF
-
最近一次修改的LSN: FIL_PAGE_LSN ,占用8Byte(与RedoLog有关)
-
已被刷到磁盘的LSN: FIL_PAGE_FILE_FLUSH_LSN ,占用8Byte(与RedoLog有关)
-
校验和: FIL_PAGE_SPACE_OR_CHKSUM ,用于页的完整性校验
页尾 – File Trailer
-
最近一次修改的LSN(LSN:日志序列号。可以把它想象成数据库的“时间戳”或“版本号”,但它比普通的时钟时间更精确、更底层。)
-
校验和:对应页头中的校验和
如果在数据传输的过程中数据丢失或异常中断,导致一个数据页不完整就可以通过页头和页尾的校验和进行验证,验证算法默认使用 CRC32
3.3.1 什么是LSN?
LSN:是"Log Sequence Number"的缩写,表示日志序号。用一个任意的、不断增加的值表示日志中记录的操作对应的时间点,用8字节的无符号长整形表示,后面会详细介绍如何生成LSN的值
3.3.2 除了页头和页尾,数据页中还有哪些信息?
页头和页尾中的各个字段描述了当前页的类型以及在文件系统中的位置,也就是说通过页头可以找到对应的页。
数据页的主要功能是保存数据,在一个数据页中,除了页头与页尾占用的46个字节之外的空间都用来存储真正的数据,也就是数据行。
数据行会与表里的数据行一一对应,基于这一特性MySQL也被称为 “行式数据库” ,也可以把除了页头页尾的区域称为页主体。
3.3.3 页主体中包含哪些信息?
页主体中的信息都是和数据相关的,其中包括刚才提到了数据行,还有为了提高查询效率的页目录 Page Directory 和为了方便操作和管理数据页的数据页头 Page Header ,这又是三个非常重要的概念,接下来我们逐个讨论。
3.4 数据行有哪些信息组成?
数据行主要存储真实数据,为了方便数据的管理与描述,InnoDB在每个数据行中还添加了一些额外(管理)信息,于是每一个 DYNAMIC 数据行都可以划分为两部分,一部分存储额外信息,一部分存储真实数据,额外信息部分包含变长字段长度列表和NULL值列表两个大小不确定的区域,以及固定占5字节及40BIT的头信息区域,头信息中存储了行的基本信息,包括行在页内的位置heap_no 、行类型 record_type 、下一行的地址偏移量 next_record 等6项信息,如下图所示:

总结:
3.4.1 数据行是如何组织在一起的?
数据行通过下一行的地址偏移量,即 next_record 将页内所有数据行组成了一个单向链表,这里要注意的是,地址偏移量指向的是下一行中真实数据的起始地址,这样做的好处是:向右是真实数据,向左就是头信息,而无需额外的长度计算。
如图所示:

3.4.2 怎么标识新页中的第一行和最后一行?
了解了行的基本结构和组织方式之后,那么当遍历页中的行时,从哪里开始到哪里结束呢?
为了解决这个问题,每当创建一个新页,都会自动分配两个行:(这个页的第一个行和最后一个行)
这两个行并不存储任何真实信息,而是做为数据行链表的头和尾,虽然不存储真实数据,但它们的数据结构和真实数据行完全一致,只不过数据区域存储的是代表它们身份的固定字符串Infimun 和 Supremun 。
新页中没有数据时,最小行 Infimun 的 next_record 直接连接最大行 Supremun ,最大行不连接任何行,它的 next_record 为0。
遍历到record_type=3,或者next_record=0的时候,就代表遍历完了,就可以去下一页了。

3.4.3 当向一个新页插入数据时是如何执行的?
当向一个新页插入数据时, heap_no 会从 2 号开始递增,表示当前记录在页面堆中的相对位置。
如果是真实数据则 record_type 为0,如果是索引目录(B+树非叶节点)数据则record_type 为1。
再将 Infimun 连接第一个数据行,最后一行真实数据行连接Supremun ,这样数据行就构建成了一个单向链表,更多的行数据插入后,会按照主键从小到大的顺序进行链接。
为了使页的结构更加清晰,通常将页中有数据行的区域称为用户数据区 User Records ,把未被数据行占用的区域称为空闲区 Free Space ,如下图所示:

3.5 如果要查询的数据在某一个页中,如何定位它在页中的位置,一条条遍历吗?
当然不是,InnoDB使用了另一种方式,更高效的查询数据,下面我们分析一下。
3.5.1 一条条遍历的查询效率高不高?
从头开始遍历是一个最简单的方法,也可以实现数据的查找,当按主键或索引查找某条数据时,从头行 infimun 开始,沿着链表顺序逐个比对查找,但一个页有16KB,通常会存在数百行数据,每次都要遍历数百行,无法满足高效查询。
3.5.2 如何提高页内的查询效率?页目录
-
为了提高查询效率,InnoDB采用二分查找来解决查询效率问题。
-
具体实现方式是,在每一个页中加入一个叫做页目录 Page Directory 的结构:将页内包括头行、尾行在内的所有行进行分组,约定头行单独为一组,其他每个组最多8条数据。
同时把每个组最后一行在页中的地址,按主键从小到大的顺序记录在页目录中。
页目录中的每一个位置称为一个槽,每个槽都对应了一个分组,一个分组里面最多可以有八个数据行。
这样在插入数据行完成链接后,一旦最后一个分组中的数据行超过分组的上限8个时,就会分裂出一个新的分组。
为了快速判断每个分组是否达到了8个的上限,在每个分组最后一行中用 n_owned 记录了这个分组内的行数,与此同时在页目录中创建一个新的槽,后续插入的行都遵守这个规则。
例子1:
如果一个分组里有八个数据行,又插入一个数据行
那么InnoDB 会将这个包含 8 条记录的分组一分为二,分裂后,会形成两个新的分组:
- 一个分组包含 4 条记录。
- 另一个分组包含 5 条记录(因为 8 = 4 + 4,但新插入的记录会进入其中一个分组,所以是 4 和 5)。
- 这么做可以确保了页目录的槽数量不会过多,从而保证了二分查找的高效性。
例子2:
槽 0: 1 条 槽 1: 4 条 槽 2: 4 条,那么插入一个数据行呢?
InnoDB 会根据新插入记录的主键值,找到它应该插入的分组。假设新记录的主键值在槽 1 和槽 2 管理的记录范围内,那么它会被插入到槽 2 管理的分组中。
注意:主键值在页内是递增的,但在整个 B+ 树的插入过程中,新记录的位置是由其主键值决定的,不一定总是在“后面”的槽。
-
后续在查询某行时,就可以通过二分查找,先找到对应的槽,然后在槽内最多8个数据行中进行遍历即可,从而大幅提高了查询效率;
-
例如要查找主键为6的行,先比对槽中记录的主键值,定位到最后一个槽2,再从最后一个槽中的第一条记录遍历,第二条记录就是我们要查询的目标行。

总结:
为了提高查询效率,在每一个页中加入一个叫做页目录 Page Directory 的结构,采用二分查找来解决查询效率问题。
3.6 关于事务、索引这些信息在页中怎么记录?

3.7 数据页的完整结构是什么样的?
注意:这里讲的是InnoDB的数据页结构,和MyISAM的页结构有所不同
下图是数据页的完整结构,以及所占的磁盘空间:






