MySQL——InnoDB引擎数据存储结构 页
1、数据库的存储结构——页
索引结构给我们提供了高效的索引方式,索引信息以及数据记录都是存储在页结构中。另一方面,索引是在存储引擎中实现的,MySQL服务器上的存储引擎负责对表中数据的进行读取和写入。不同存储引擎中存放数据的格式一般是不同的,甚至有的存储引擎比如Memory都不用磁盘来存储数据。
页的概述
数据库读写磁盘的基本单位是页(Page),数据库,无论是读一行,还是读取多行,都是将这些行所在的页进行加载。
因为记录是按照行来存储的,但是数据库的读取并不以行为单位,否则一次I/O操作只能处理一行数据,效率会很低。
lnnoDB将数据划分为若干个页,InnoDB中页的大小默认为16KB。
以页作为磁盘和内存之间交互的基本单位,也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB的内容刷新到磁盘中。
页结构
不同的页可以不在物理结构上相连,只要通过双向链表相关联即可,每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里边的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
页的大小
不同的DBMS的页大小不同。在MySQL的InnoDB中,默认页大小是 16KB。
查看默认页大小:
show variables like '%innodb_page_size%';
SQL Server中页的大小为 8KB,而在Oracle中我们用术语“块”(Block)来代表“页”,Oralce支持的块大小为 2KB,4KB,8KB,16KB,32KB 和 64KB。
页的上层结构
在数据库存储结构中,还存在表空间(tablespace),段(segment),区(extent),行(row)的概念
区(Extent)是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64 个连续的页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB= 1MB。
段(Segment)由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。
表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。
2、页的内部结构
页如果按照类型划分的话,常见的有 数据页,系统页、Undo日志页和事务数据页等。数据页是我们最常使用的页。
数据页的16KB大小的存储空间被划分为七个部分,分别是文件头(File Header)、页头(Page Header)、最大最小记录(Infimum+supremum)、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer)。
页结构的大致示意图如下所示:
七个部分大致功能说明如下:
- File Header 文件头:记录各种页的通用信息,比如上下页的页号,页类型,所有的数据页其实是一个双链表
- Page Header 页头:记录本页存储记录的状态信息,比如本页记录数量,槽数量
- Infimum + supremum 最小与最大记录,是两个虚拟的行记录
- User Records 用户记录:真正存数据的地方:以链表的形式存储一条条行记录
- Free Space 存数据空间中尚未使用的区域
- Page Directory 页目录:页中某些记录的相对位置,用于提升查询效率
- File Trailer 文件尾:刷盘时校验页是否完整
2.1、File Header 文件头和 File Trailer 文件尾
File Header 文件头
File Header 文件头构成:
名称 | 大小 | 描述 |
---|
FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | 页的校验和(checksum),文件尾也有这个属性,用于自校验。为了快速比较、保证数据的完整性防止遭到破坏等,采用给这页加上校验和到页尾的时候做自校验。 | FIL_PAGE_OFFSET | 4字节 | 页号,每一个页都都有一个单独的页号,InnoDB通过页号可以定位到这个页 | FIL_PAGE_PREV | 4字节 | 上一个页的页号 | FIL_PAGE_NEXT | 4字节 | 下一个页的页号,保证了页之间是逻辑上的连续 | FIL_PAGE_LSN | 8字节 | 页面最后被修改的日志序列位置 | FIL_PAGE_TYPE | 2字节 | 该页的类型 | FIL_PAGE_FILE_FLUSH_LSN | 8字节 | 独立表空间中都是0 | FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
File Trailer 文件尾
File Trailer 文件尾(8个字节)构成:
- 前4个字节代表页的校验和,这个部分和 File Header 中的校验和相对应
- 后4个字节代表页面被最后修改时对应的日志序列位置(LSN),这个部分也是为了校验页的完整性的,如果首部和尾部的LSN值校验不成功的话,就说明同步传输过程出现了问题。
2.2、User Records(用户记录)和 Infimum + Supremum(最小最大记录)
页的主要作用是存储记录,所以“最大最小记录” 和 “用户记录” 部分占了页结构的主要空间。
User Records(用户记录)
User Records是用来存储数据的地方,简单来说就是怎么把每行数据摆在这个空间里。
当我们新建一个表的时候表中用户记录(用户记录)部分是空的,在外面插入一条记录后会被记录到其中,直到插入满是会把记录信息刷入到下一个页中,往复循环。
User Records 中的这些记录按照指定的行格式一条一条摆放在 User Records 部分,相互之间形成单链表。
记录行格式的记录头信息在摆放数据的过程中发挥了重要的作用,下面是记录头的各个属性:
-
delete_mask 1bit 标记该记录是否被删除,
-
min_rec_mask 1bit B+数的每非叶子节点中的最小纪录数都会添加该标记,
- 只有最小纪录数的min_rec_mask 值为1,
- 其他别的记录min_rec_mask 值为0
-
n_owned 4bit 如果当前记录是组内最大记录,则代表槽内的记录数 -
heap_no 13bit 当前记录在本页中的位置信息 -
record_type 3bit 表示当前记录的类型,
- 0表示普通记录,
- 1表示B+树非叶子节点记录,
- 2表示最小记录,
- 3表示最大记录
-
next_record 16bit 表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。 比如:第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址处向后找32个字节便是下一条记录的真实数据。
被删除的记录为什么还在页中存储呢?
你以为它删除了,可它还在真实的磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后其他的记录在磁盘上需要重新排列,导致性能消耗。所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
关于heap_no值为0和1的记录
MySQL会自动给每个页里加了两个记录,由于这两个记录并不是我们自己插入的,所以有时候也称为伪记录或者虚拟记录。这两个伪记录一个代表最小记录,一个代表最大记录。最小记录和最大记录的heap_no值分别是0和1,也就是说它们的位置最靠前。
Infimum + Supremum(最小最大记录)
比较记录的大小就是比较主键的大小。
InnoDB规定的最小记录与最大记录这两条记录的构造十分简单,都是由5字节大小的记录投信息和8个字节大小的一个固定的部分组成的:
定Infimum记录(也就是最小记录)的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是Supremum记录(也就是最大记录)。
2.3、Page Directory(页目录)和 Page Header(页面头部)
Page Directory(页目录)
现在有一个问题,我们要在一个页中查找指定的一条记录。除了从顺序查找还有更高效率的方法么?
Page Directory提供了解决方案:二分查找法。
InnoDB 会将一个页中的所有记录划分成若干个组,每组4-8个记录。将每个组最后一个记录相对于第一个记录的地址偏移量(可以定位到真实数据记录)提取出来存放在页中一个叫做Page Directory的数组中,数组中的元素就是这些地址偏移量,也称为槽(slot)。所以Page Directory就是由槽组成的。
所以在一个页中根据主键查找记录是很快的,步骤为两步:
- 二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
- 通过记录的 next_record 属性遍历该槽所在的组中的各个记录
Page Header(页面头部)
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,这个部分占用固定的56个字节,专门存储各种状态信息。
各字段如下:
名称 | 占用空间大小 | 描述 |
---|
PAGE_N_DIR_SLOTS | 2字节 | 页目录中的槽数量 | PAGE_HEAP_TOP | 2字节 | 还未使用的空间最小地址,该地址之后就是 Free Space | PAGE_N_HEAP | 2字节 | 本页中的记录的数量(包括最大最小和标记为删除的记录) | PAGE_FREE | 2字节 | 指向可重用空间的地址 | PAGE_GARBAGE | 2字节 | 已删除记录占用的字节数 | PAGE_LAST_INSERT | 2字节 | 最后插入记录的位置 | PAGE_DIRECTION | 2字节 | 最后插入的方向 | PAGE_N_DIRECTION | 2字节 | 一个方向连续插入的记录数量 | PAGE_N_RECS | 2字节 | 该页中记录的数量(不包括最大最小和标记为删除的记录) | PAGE_MAX_TRX_ID | 2字节 | 修改当前页的最大事务ID,该值仅在二级索引中定义 | PAGE_LEVEL | 2字节 | 当前页在索引树(B+树)中的层级 | PAGE_INDEX_ID | 8字节 | 索引ID,表示当前页属于哪个索引 | PAGE_BTR_SEG_LEAF | 10字节 | B+树叶子段的头部信息,仅在B+树的Root页定义 | PAGE_BTR_SEG_TOP | 10字节 | B+树叶子段的头部信息,仅在B+树的Root页定义 |
假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态就是PAGE_DIRECTION。
假设连续几次插入新记录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。
|