概念
在 InnoDB 存储引擎中,所有的数据都被逻辑地存放在表空间中,表空间(tablespace)是存储引擎中最高的存储逻辑单位,在表空间的下面又包括段(segment)、区(extent)、页(page),他们之间的关系如下:
数据页
页是mysql中磁盘和内存交换的基本单位,也是mysql管理存储空间的基本单位。
同一个数据库实例的所有表空间都有相同的页大小;默认情况下,表空间中的页大小都为 16KB,当然也可以通过改变 innodb_page_size 选项对默认大小进行修改,需要注意的是不同的页大小最终也会导致区大小的不同。
一次最少从磁盘读取16KB内容到内存中,一次最少把内存中16KB内容刷新到磁盘中,当然了单页读取代价也是蛮高的,一般都会进行预读
名称 | 中文名 | 占用空间大小 | 简单描述 |
| 文件头部 | 38字节 | 页的一些通用信息 |
| 页面头部 | 56字节 | 数据页专有的一些信息 |
| 最小记录和最大记录 | 26字节 | 两个虚拟的行记录 |
| 用户记录 | 不确定 | 实际存储的行记录内容 |
| 空闲空间 | 不确定 | 页中尚未使用的空间 |
| 页面目录 | 不确定 | 页中的某些记录的相对位置 |
| 文件尾部 | 8字节 | 校验页是否完整 |
Infimum + Supremum
- 这个值位于整个页面的第三部分
- 分别是最小记录和最大记录,属于MySQL为每个页添加的
虚拟记录
- 由五个字节的
记录头
和 八个字节的 值(分别是单词Infimum
和Supremum
)组成 - 最小记录的记录头中
heap_no
为0 - 最大记录的记录头中
heap_no
为1 - 也就是说正式记录中的
heap_no
属性从2开始 - 最小记录的record_type 是2
- 最大记录的record_type 是3
- 最小记录是页中单链表的头结点
- 最大记录是页中单链表的尾结点
Page Directory
- 这个页目录里面存的是一个一个的槽(
slot
) - 每个槽指向组内最大记录的地址偏移量(我理解为页内偏移量)
- 这个数据结构是数组,按主键值从小到大排列
- 这种结构注定了查询最快的方式是二分法,时间复杂度O(lgN)
-
页分裂
(前面说过) 时会增加一个槽
User Records
和 Free Space
- 完全空闲的页是没有
User Records
部分的 - 插入数据时,从
Free Space
分配空间给User Records
,直到Free Space
没有空间或空间不够分配新的记录,这时需要申请新的页
Page Header
名称 | 占用空间大小 | 描述 |
| 2字节 | 在页目录中的槽数量 |
| 2字节 | 还未使用的空间最小地址,也就是说从该地址之后就是Free Space |
| 2字节 | 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
| 2字节 | 第一个已经标记为删除的记录地址(各个已删除的记录通过next_record也会组成一个单链表,这个单链表中的记录可以被重新利用) |
| 2字节 | 已删除记录占用的字节数 |
| 2字节 | 最后插入记录的位置 |
| 2字节 | 记录插入的方向 |
| 2字节 | 一个方向连续插入的记录数量 |
| 2字节 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
| 8字节 | 修改当前页的最大事务ID,该值仅在二级索引中定义 |
| 2字节 | 当前页在B+树中所处的层级 |
| 8字节 | 索引ID,表示当前页属于哪个索引 |
| 10字节 | B+树叶子段的头部信息,仅在B+树的Root页定义 |
| 10字节 | B+树非叶子段的头部信息,仅在B+树的Root页定义 |
1.PAGE_GARBAGE
- 在页面空间不够时,会尝试将所需空间与这个值比较,如果够,那么将记录移到临时表中,再重新插回来(记录逐条新增是不会产生碎片空间的),但很明显,这很耗性能
-
PAGE_BTR_SEG_LEAF
和 PAGE_BTR_SEG_TOP
- 后面会说到段的概念,一个索引会有两个段,一个叶子节点段,一个非叶子节点段,这两个属性分别代表一个
Segment Header
,代表了哪个表空间哪个页面的哪个INODE Entry
(表空间号 + 页面号 + 偏移量值)
File Header
名称 | 占用空间大小 | 描述 |
| 4字节 | 页的校验和(checksum值) |
| 4字节 | 页号 |
| 4字节 | 上一个页的页号 |
| 4字节 | 下一个页的页号 |
| 8字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number) |
| 2字节 | 该页的类型 |
| 8字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
| 4字节 | 页属于哪个表空间 |
这个属性里面的值就说三个:
-
FIL_PAGE_OFFSET
- 每个表空间的页号唯一,四个字节足够用了
-
FIL_PAGE_PREV
和 FIL_PAGE_NEXT
- 上一页和下一页,诶哟,这不是双向链表啊,那所有的页加起来组成了一个双向链表哇,好玩吧~
-
FIL_PAGE_TYPE
类型名称 | 十六进制 | 描述 |
| 0x0000 | 最新分配,还没使用 |
| 0x0002 | Undo日志页 |
| 0x0003 | 段信息节点 |
| 0x0004 | Insert Buffer空闲列表 |
| 0x0005 | Insert Buffer位图 |
| 0x0006 | 系统页 |
| 0x0007 | 事务系统数据 |
| 0x0008 | 表空间头部信息 |
| 0x0009 | 扩展描述页 |
| 0x000A | BLOB页 |
| 0x45BF | 索引页,也就是我们所说的数据页 |
现在只需要看这最后一个索引页,又称数据页(跟InnoDB
的特性有关)
属性介绍了一堆,还是不知道怎么查,举个栗子
- 假设一条查询语句定位到了这个页面
- 先将条件中的列(加深为
col
)与值(假设为x
)拎出来,二分法定位在哪个组。
- 假设10个槽,(0 + 9)/2 = 4, 将第四个槽的条件列的值拿出来跟
x
比较,如果x
大,那就从5~9的槽中继续二分找;如果x
小, 那就从0~4的槽中继续二分; - 直到前后两个槽相差为1,那记录就在那个大的槽里面,又每个槽其实存的是组内最大记录,那前一个槽对应记录的下一个节点就是目标槽对应组中的最小值,顺着单链表往后一个一个比就完事了,每组最多八条记录,这速度还是很快的。