【MySQL】 InnoDB
【MySQL】 InnoDB
Metadata
title: 【MySQL】 InnoDB
date: 2022-12-20 16:12
tags:
- 行动阶段/完成
- 主题场景/数据存储
- 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
- 细化主题/数据存储/MySQL
categories:
- 数据存储
keywords:
- 数据存储
description: MySQL 默认的事务型存储引擎,**只有在需要它不支持的特性时,才考虑使用其它存储引擎。**
概述
InnoDB是MySQL的默认事务型存储引擎,除非需要其他引擎所支持的特性,否则应该优先考虑使用InnoDB。
行格式
- InnoDB将数据划分为以页为单位的基本交互单元,页的大小通常为16KB。
- 行格式是指以记录为单位向表中插入数据的方式,也被称为记录格式。InnoDB支持多种行格式,包括Compact、Redundant、Dynamic和Compressed。
- COMPACT行格式是InnoDB中的一种行格式,记录的额外信息包括变长字段长度列表和NULL值列表。变长字段长度列表存储了变长字段的真实数据占用的字节长度,NULL值列表表示记录中允许存储NULL值的列。
- 记录头信息是固定的5个字节,包含了记录的一些标识信息,如是否被删除、记录的拥有者数量等。
- 记录的真实数据包括隐藏列和用户自定义列,隐藏列包括row id、transaction_id和roll_pointer。
- InnoDB对主键的生成策略是优先使用用户自定义主键,如果没有定义主键则选择Unique键,如果连Unique键都没有则默认添加一个名为row_id的隐藏列作为主键。
- CHAR(M)列的存储格式取决于采用的字符集,定长字符集不会被加到变长字段长度列表,而变长字符集会被加到变长字段长度列表。
- REDUNDANT行格式是InnoDB中的另一种行格式,包括字段长度偏移列表和记录头信息。字段长度偏移列表按逆序存储了记录中各列的长度信息。
- 行溢出数据指记录中超出页空间限制的部分,如VARCHAR(M)类型的数据。行溢出数据存储在其他页中,存储在记录中的是该列的部分数据和指向溢出页的地址。
- DYNAMIC和COMPRESSED行格式是InnoDB中的其他行格式,它们在处理行溢出数据时与COMPACT和REDUNDANT有所不同,DYNAMIC和COMPRESSED行格式会将所有字节存储在其他页面中,并在记录的真实数据处存储其他页面的地址。COMRESSED行格式还使用压缩算法对页面进行压缩以节省空间。
数据页
- InnoDB为了不同的目的而设计了不同类型的页,我们把用于存放记录的页叫做数据页。
- 一个数据页可以被大致划分为7个部分,分别是
- File Header ,表示页的一些通用信息,占固定的38字节。
- Page Header ,表示数据页专有的一些信息,占固定的56个字节。
- Infimum + Supremum ,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的26 个字节。
- User Records :真实存储我们插入的记录的部分,大小不固定。
- Free Space :页中尚未使用的部分,大小不确定。
- Page Directory :页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。
- File Trailer :用于检验页是否完整的部分,占用固定的8个字节。
- 每个记录的头信息中都有一个next_record 属性,从而使页中的所有记录串联成一个单链表。
- InnoDB 会为把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory 中,所以在一个页中根据主键查找记录是非常快的,分为两步:
- 通过二分法确定该记录所在的槽。
- 通过记录的next_record属性遍历该槽所在的组中的各个记录。
- 每个数据页的File Header 部分都有上一个和下一个页的编号,所以所有的数据页会组成一个双链表。
- 为保证从内存中同步到磁盘的页的完整性,在页的首部和尾部都会存储页中数据的校验和和页面最后修改时对应的LSN 值,如果首部和尾部的校验和和LSN 值校验不成功的话,就说明同步过程出现了问题。
InnoDB
MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。
将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为16KB。
行格式
以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。
分别是Compact、Redundant、Dynamic和Compressed行格式
COMPACT行格式
记录的额外信息
服务器为了描述这条记录而不得不额外添加的一些信息
变长字段长度列表
变长字段占用的存储空间:
- 真正的数据内容
- 占用的字节数
把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放
表示真实数据占用的字节数, InnoDB 有它的一套规则,我们首先声明一下W 、M 和L 的意思:
- 假设某个字符集中表示一个字符最多需要使用的字节数为W, 如 utf-8 W=3
- 对于变长类型VARCHAR(M) 来说,这种类型表示能存储最多M 个字符, 这个类型能表示的字符串最多占用的字节数就是M×W
- 假设它实际存储的字符串占用的字节数是L 。
规则
- 如果M×W <= 255 ,那么使用1个字节来表示真正字符串占用的字节数。
- 如果M×W > 255 ,则分为两种情况:
- 如果L <= 127 ,则用1个字节来表示真正字符串占用的字节数。
- 如果L > 127 ,则用2个字节来表示真正字符串占用的字节数。
总结一下就是说:如果该可变字段允许存储的最大字节数( M×W )超过255字节并且真实存储的字节数( L )超过127字节,则使用2个字节,否则使用1个字节。
变长字段长度列表中只存储值为 非NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的
NULL值列表
- 首先统计表中允许存储NULL 的列有哪些。
- 如果表中没有允许存储 NULL 的列,则 NULL值列表 也不存在了,否则将每个允许存储NULL 的列对应一个二进制位,二进制位按照列的顺序逆序排列
- MySQL 规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0 。
记录头信息
固定的5 个字节组成。5 个字节也就是40 个二进制位,不同的位代表不同的意思
名称 | 大小(单位: bit) | 描述 |
---|---|---|
预留位1 | 1 | 没有使用 |
预留位2 | 1 | 没有使用 |
delete_mask | 1 | 标记该记录是否被删除 |
min_rec_mask | 1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned | 4 | 表示当前记录拥有的记录数 |
heap_no | 13 | 表示当前记录在记录堆的位置信息 |
record_type | 3 | “表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3 表示最大记录” |
next_record | 16 | 表示下一条记录的相对位置 |
记录的真实数据
MySQL 会为每个记录默认的添加一些列(也称为隐藏列)
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row id | 否 | 6字节 | “行ID,唯一标识一条记录” |
transaction_id | 是 | 6字节 | 事务ID |
roll_pointer | 是 | 7字节 | 回滚指针 |
InnoDB 表对主键的生成策略
- 优先使用用户自定义主键作为主键
- ,如果用户没有定义主键,则选取一个Unique 键作为主键
- 如果表中连Unique 键都没有定义的话,则InnoDB 会为表默认添加一个名为row_id 的隐藏列作为主键
InnoDB存储引擎会为每条记录都添加 transaction_id和 roll_pointer 这两个列,但是 row_id 是可选的
CHAR(M)列的存储格式
对于 CHAR(M) 类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,
而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。
Redundant行格式
字段长度偏移列表
- Redundant 行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表
- 多了个偏移两个字,这意味着计算列值长度的方式不像Compact 行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。
记录头信息
Redundant 行格式的记录头信息占用6 字节, 48 个二进制位
Redundant 行格式中NULL 值的处理
字段长度偏移列表中的各个列对应的偏移量处做了一些特殊处理 —— 将列对应的偏移量值的第一个比特位作为是否为NULL 的依据,该比特位也可以被称之为NULL比特位。
在解析一条记录的某个列时,首先看一下该列对应的偏移量的NULL比特位是不是为1 ,如果为1 ,那么该列的值就是NULL ,否则不是NULL 。
CHAR(M)列的存储格式
只要是使用CHAR(M) 类型,占用的真实数据空间就是该字符集表示一个字符最多需要的字节数和M 的乘积。
行溢出数据
VARCHAR(M)最多能存储的数据
M 最大值是 65535 / W
记录中的数据太多产生的溢出
在本记录的真实数据处只会存储该列的前768 个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做行溢出,存储超出768 字节的那些页面也被称为溢出页。
行溢出的临界点
MySQL 中规定一个页中至少存放两行记录
一个行中存储了很大的数据时,可能发生行溢出的现象
Dynamic和Compressed行格式
版本是5.7 ,它的默认行格式就是Dynamic
这俩行格式和Compact 行格式挺像,只不过在处理行溢出数据时有点儿分歧
把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址
Compressed 行格式和Dynamic 不同的一点是, Compressed 行格式会采用压缩算法对页面进行压缩,以节省空间。
InnoDB数据页结构
不同类型的页简介
一个页的大小一般是16KB
InnoDB 为了不同的目的而设计了许多种不同类型的页,比如存放表空间头部信息的页,存放Insert Buffer信息的页,存放INODE 信息的页,存放undo 日志信息的页等等等等
存放记录的页为索引( INDEX )页
数据页结构的快速浏览
名称 | 中文名 | 占用空间大小 | 简单描述 |
---|---|---|---|
File Header | 文件头部 | 38字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56 字节 | 数据页专有的一些信息 |
Infimum + Supremum | 最小记录和最大记录 | 26字节 | 两个虚拟的行记录 |
User Records | 用户记录 | 不确定 | 实际存储的行记录内容 |
Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
Page Directory | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
File Trailer | 文件尾部 | 8 字节 | 校验页是否完整 |
记录在页中的存储
记录头信息的秘密
- delete_mask
这个属性标记着当前记录是否被删除,占用1个二进制位,值为0 的时候代表记录并没有被删除,为1 的时候代表记录被删除掉了。
这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗
所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
- min_rec_mask
B+树的每层非叶子节点中的最小记录都会添加该标记
- heap_no
自动给每个页里边儿加了两个记录,由于这两个记录并不是我们自己插入的,所以有时候也称为伪记录或者虚拟记录
这两个伪记录一个代表最小记录,一个代表最大记录
最小记录和最大记录的heap_no 值分别是0 和1
- record_type
表示当前记录的类型
0 表示普通记录
1 表示B+树非叶节点记录 (目录项记录)
2 表示最小记录
3 表示最大记录
- next_record
从当前记录的真实数据到下一条记录的真实数据的地址偏移量
下一条记录指得是按照主键值由小到大的顺序的下一条记录
规定 Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)
Page Directory(页目录)
- 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
- 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned 属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
- 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory ,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽(英文名: Slot ),所以这个页面目录就是由槽组成的。
对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 18 条之间,剩下的分组中记录的条数范围只能在是 48 条之间。
分组是按照下边的步骤进行的
初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned 值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
在一个数据页中查找指定主键值的记录的过程分为两步
- 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
- 通过记录的next_record 属性遍历该槽所在的组中的各个记录。
Page Header(页面头部)
Page Header 是专门针对数据页记录的各种状态信息,比方说页里头有多少个记录了呀,有多少个槽
名称 | 描述 | 占用空间大小 |
---|---|---|
PAGE_N_DIR_SLOTS | 2字节 | 在页目录中的槽数量 |
PAGE HEAP_TOP | 2字节 | “还未使用的空间最小地址,也就是说从该地址之后就是Free Space” |
PAGE_N_HEAP | 2字节 | 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
PAGE_FREE | 2字节 | “第一个已经标记为删除的记录地址(各个已删除的记录通过next_record 也会组成一个单链表,这个单链表中的记录可以被重新利用)” |
PAGE_GARBAGE | 2字节 | 已删除记录占用的字节数 |
PAGE_LAST_INSERT | 2字节 | 最后插入记录的位置 |
PAGE_DIRECTION | 2字节 | 记录插入的方向 |
PAGE_N_DIRECTION | 2字节 | 一个方向连续插入的记录数量 |
PAGE_N_RECS | 2字节 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
PAGE_MAX_TRX_ID | 8字节 | “修改当前页的最大事务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
新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态就是PAGE_DIRECTION 。
- PAGE_N_DIRECTION
InnoDB 会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION 这个状态表示
File Header(文件头部)
描述了一些针对各种页都通用的一些信息
名称 | 占用空间大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | 页的校验和 (checksum值) |
FIL_PAGE_OFFSET | 4字节 | 页号 |
FIL_PAGE PREV | 4字节 | 上一个页的页号 |
FIL_PAGE_NEXT | 4字节 | 下一个页的页号 |
FIL PAGE LSN | 8字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number) |
FIL_PAGE_TYPE | 2字节 | 该页的类型 |
FIL_PAGE FILE FLUSH LSN | 8字节 | “仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值” |
FIL_ PAGE ARCH_ LOG NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
- FIL_PAGE_SPACE_OR_CHKSUM
当前页面的校验和(checksum)
- FIL_PAGE_OFFSET
每一个页都有一个单独的页号,就跟你的身份证号码一样, InnoDB 通过页号来可以唯一定位一个页。
- FIL_PAGE_TYPE
这个代表当前页的类型
- FIL_PAGE_PREV 和FIL_PAGE_NEXT
FIL_PAGE_PREV 和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号
并不是所有类型的页都有上一个和下一个页的属性
File Trailer
每个页的尾部都加了一个File Trailer 部分
- 前4个字节代表页的校验和
- 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)