从InnoDB存储引擎的逻辑存储结构来看,所有的数据都被逻辑逻辑的存放在一个空间中,成为表空间。表空间又由 段、区、页组成。本文着重将页和行记录相关的数据
mysql服务器是我们在生产环境中常用的服务器,大部分的存储引擎数据都是写入到磁盘中的而服务器每次读取数据时会先从pagecache
读取数据,如果pagecache
中没有,需要从磁盘进行读取。
我们知道当我们从磁盘读取数据时,需要经过磁盘寻址加旋转的过程,相对于从缓存中读数据来说是很慢的,所以为了保证数据的读取效率,mysql服务器不是按需读取,而是进行磁盘预读
预读的长度一般为页(page)的整倍数。磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中。
页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。
InnoDB中页的大小一般为 16 KB,也就是说,当需要从磁盘中读数据时 每一次最少将从磁盘中读取16KB的内容到内存中,每一次最少也会把内存中的16KB内容写到磁盘中。
默认的页大小为 16KB,每个页中至少存储有 2 条或以上的行记录
从InnoDB 1.2X开始 我们可以通过参数 innodb_page_size
将页的大小设置为4K、8K、16K
SHOW GLOBAL STATUS like 'Innodb_page_size';
名称 | 占用空间 | 描述 |
---|---|---|
File Header | 38字节 | 页的一些通用信息 |
Page Header | 56字节 | 数据页专有的一些信息 |
Infimum + Supremum | 26字节 | 两个虚拟的行记录 |
User Records | 不确定 | 实际存储的行记录内容 |
Free Space | 不确定 | 页中尚未使用的空间,被删除的行记录会被记录成空闲空间 |
Page Directory | 不确定 | 页中的某些记录的相对位置,二叉查找相关的信息 |
File Trailer | 8字节 | 存储用于检测数据完整性的校验和等数据 |
各种页的通用信息,固定为38个字节
名称 | 占用空间大小(字节) | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | 页的校验和(checksum值) |
FIL_PAGE_OFFSET | 4 | 页号,InnoDB通过页号来可以唯一定位一个页 |
FIL_PAGE_PREV | 4 | 上一个页的页号 |
FIL_PAGE_NEXT | 4 | 下一个页的页号 |
FIL_PAGE_LSN | 8 | 页面被最后修改时对应的日志序列位置(Log Sequence Number) |
FIL_PAGE_TYPE | 2 | 该页的类型 记住0x45BF 代表数据页 |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 对于独立表空间该值都为0 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 页属于哪个表空间 |
其中比较重要的是 FIL_PAGE_PREV 和 FIL_PAGE_NEXT 字段,通过这两个字段,我们可以找到该页的上一页和下一页,实际上所有页通过两个字段可以形成一条双向链表
并不是所有页都有这个属性的,但是索引页有。
Page Header 字段用于记录 Page 的状态信息
如下结构仅针对索引页
名称 | 占用空间大小(字节) | 描述 |
---|---|---|
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这个状态上。
如果最后一条记录的插入方向改变了,这个状态的值会被清零。
Infimum 和 Supremum 是两个伪行记录,Infimum(下确界)记录比该页中任何主键值都要小的值,Supremum (上确界)记录比该页中任何主键值都要大的值,这个伪记录分别构成了页中记录的边界。
存储二分查找槽的信息 (页的相对位置而不是偏移量)
按主键查找的顺序
1.先通过二分查找定位主键所在的slot
2.找到slot中最小的行记录
3.根据记录上的next_record属性遍历整个slot
4.返回
需要牢记的是B+数索引本身并不能找到具体一条记录,能找到的是该记录所在的页。数据库把页加载到内存,然后通过 Page Directory 再进行二分查找。只不过二分查找的复杂度低,同时在内存中的查找很快,因此通常忽略这部分查找所用的时间
InnoDB存储引擎是面向列的。也就是说数据是按行存储的。
每个页存放的行记录也是有硬性定义的,最多允许存放 16K/2-200行的记录,即7992行记录。
show table status like 'tableName'
来查看行格式CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
主要包含三部分:变成字段列表,NULL值列表,记录头信息
变长字段列表中存储多少字节的数据不是固定的,实际长度取决于列数和每一列的长度。
比如由 VARCHAR(M)、VARBINARY(M)、TEXT类型,BLOB类型等数据类型修饰的列称为变长字段
注意VARCHAR(M),M代表最大能存多少个字符。( MySQL5.0.3以前是字节,以后就是字符)
字节长度需要跟编码方式相关联,例如 UTF-8 一个中文字符需要 3 字节来表示,ascii一个字符就是一个字节
记录变长数据的真实字节数
在Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表。
1.变长字段实际占用字节数以逆序方式存储在变长字段长度列表中
2.允许的最大字节超过255且实际存储超过127字节, 使用两个字节存储其长度, 否则使用一个字节
InnoDB在读字段变长列表时会先查表结构, 允许的最大字节数超过255时才会使用这个二进制位作为标识位来判断是读一个字节还是两个字节,没有超过就直接读一个字节也就不存在标识位了
变长字段的长度最大不可以超过两字节
3.变长列不为NULL时, InnoDB才会存储其字节长度
4.如果没有变长字段或为NULL,则当前记录没有此部分
5.第一个字节的第一位是标志位,表示是否双字节表示
一个字节 一共8位
如果有的列可以为NULL,那么 Compac行格式会把这些NULL字段统一管理起来,存一个标志位在NULL值列表中。
如果一个表中的列都不允许为NULL 则不会存在这个列表
在建表时,字段进行都设置为非NULL,这样就可以节省这一部分空间了
记录头为定长结构,长度为5字节,但是每个位都代表不同的含义
如下用来描述记录的相关信息
名称 | 长度(bit) | 作用 |
---|---|---|
预留位1 | 1 | 保留 |
预留位2 | 1 | 保留 |
delete_mask | 1 | 标记该记录是否被删除 |
min_rec_mask | 1 | 标记B+树中每层非叶子节点的最小记录 (Mysql技术内幕写的是:如果该记录是被预先定义为最小记录) |
n_owned | 4 | 当前记录拥有的记录数(这个组有多少条记录) |
heap_no | 13 | 当前记录在堆中的位置 |
record_type | 3 | 000:普通记录,001:B+树非叶子节点记录,010:最小记录,011:最大记录 1xx:保留 |
next_record | 16 | 下一条记录的相对位置 |
一个页中的所有记录使用next_record 字段形成了一条单链表
记录的真实数据中除了存放真实数据之外,还有三个隐藏列
名称 | 长度(字节) | 作用 | 是否必须 |
---|---|---|---|
DB_ROW_ID | 6 | 唯一标示一条记录 | 否 |
DB_TRX_ID | 6 | 事务ID | 是 |
DB_ROLL_PTR | 7 | 回滚指针,指向一条undo日志记录 | 是 |
只有当一个表没有手动定义主键,且没有Unique键时,才会为表默认添加一个名为row_id(DB_ROW_ID)的隐藏列作为主键。
NULL不占该部分的任何空间,即NULL除了占有NULL标志位,实际存储不占有任何空间
mysql中一条记录占用的最大存储空间是有限制的。除了Text和Blob大字段外,其余的字段(不包括隐藏列和记录头信息)加起来不能超过65535个字节。myisam存储引擎不受此限制
我们知道数据页的大小是 16KB即16384字节,Innodb 存储引擎保证了每一页至少有两条记录
一般情况下数据都存放在页类型为 B-Tree node中,但是当发生了行溢出时,会截取前768字节的前缀数据和每个溢出页的偏移量存放在B-Tree node中剩下的数据存放在多个类型为Uncompressed BLOB页中。
所以可能会存在一个页放不完数据的情况,针对这种情况,不同的行格式处理的方式不同
对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中。然后记录的真实数据处用20个字节存储指向这些页的地址,从而可以找到剩余数据所在的页。
20个字节中还包含每页的大小
它们不会在记录的真实数据处存储一部分数据,而是把所有的数据都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
Dynamic和Compressed行格式除了上述说的处理溢出问题时和别的格式不一样,其他的都几乎一样
Compressed行格式会采用压缩算法对页面进行压缩。
Mysql数据库的VARCHAR没办法存储65535字节的实际数据,这是因为还有别的开销
如果表中只有这一个字段
文当中说的支持最大VARCHAR(65535),单位是字节。
65535 是说的所有的 VARCHAR 列的总和