《MySQL系列》01 InnoDB行记录存储结构

Silently9527
2021-08-17 / 0 评论 / 79 阅读
温馨提示:
本文最后更新于2021-08-17,若内容或图片失效,请留言反馈。

程序员常用的IDEA插件:https://github.com/silently9527/Toolkit

本文已被Github仓库收录 https://github.com/silently9527/ProgrammerNotes

微信公众号:贝塔学Java

前言

我们平时在向MySQL数据库表中插入数据时,实际数据是以行记录的格式存储在磁盘上的,本篇我们就一起来详细的了解下MySQL的行记录格式,理解了行记录的格式有助于我们后面了解MySQL如何快速在页中定位出行记录,以及MySQL的版本控制链,事务隔离级别等等,行记录格式是许多MySQL核心知识的基础。

InnoDB行记录类型

MySQL中总共提供了四种类型的行格式:Compact,Redundant,Dynamic,Compressed

在创建表或修改表的时候可以指定行记录的格式
create table 表名 row_format=行格式名
alter table 表名 row_format=行格式名

知道就行,不需要去记住,基本上使用不到

Compact行格式

在四种类型的行格式中,我们主要来学习Compact格式,其他格式的行记录类似;

从图中我们可以看出行记录主要是由4部分组成:变长字段长度、Null值列表,行记录头信息以及列的真实数据

变长字段长度列表

在MySQL中很一些变长的数据类型(varchar,text等),MySQL需要知道这些数据的实际长度,这样才能正确的在真实数据中取出对应列的数据,所以变长字段是由两部分组成:

  • 真实数据的长度
  • 真实数据的字节

每个变长字段的长度要么用1字节要么用2字节表示,由此就决定了每个字段的最大字节数是65535;

  • 假如字符类型若为gbk,每个字符最多占2个字节,最大长度不能超过32766;
  • 假如字符类型若为utf8,每个字符最多占3个字节,最大长度不能超过21845。

那到底什么时候选用1字节什么时候选用2字节呢?

这里需要定义三个变量:w,m,l

  1. 假如使用的字符集是utf8mb4,每个字符占用的字节数是4字节,那么w=4;假如字符类型若为utf8,每个字符最多占3个字节,那么w=3; 所以w表示字符集中每个字符所占的字节数
  2. varchr(m),这里m表示的是定义的字符的长度
  3. l 表示的是该字段真实数据占用的字节数

m*w <= 255;表示该字段定义的最大长度都不会超过1字节,那么该字段的长度就用1字节表示

m*w > 255 && l<=127; 表示该字段定义的长度可能会超过1个字节,但是当前的实际长度是小于127的,可以用1个字节表示

m*w > 255 && l>127; 用2字节来表示该字段的长度

思考:为什么与l比较的值是127呢?
当我们定义的变长字段可能大于255(也就是超过一个字节)时,MySQL如何才能知道当前读取的字节该字段的完成字段长度,还是该字段的半个字段长度,为了解决这个问题,MySQL使用了1字节的首位,当首位为0表示当前是1字节,当首位为0表示当前长度是2字节;由于占用了1字节的首位,所以剩下7位所能表示的最大值是127

变长字段不会存储为Null列的长度;其次并不是行记录中一定需要变长字段长度这段内容,如果行记录中没有定义变长字段或者是变长字段都为Null,那么就不会有变长字段长度这部分

变长字段占用的字节数按照顺序逆序存储

Null值列表

一条记录中某些列通常可能允许为null,所以Compact行格式把这些允许为null的进行了统一管理;

  1. 首先统计出表中定义的哪些列允许为null
  2. 如果表中的字段都不能为空,那么就不存在null值列表;如果存在允许为null的字段,那么就按照字段的顺序为每个字段对应一个二进制位,当二进制位为1时表示该列值为空;当二进制位位0时表示该列值不为空
  3. Null值列表必须有整数个字节来表示,所以对应没有占用的位使用0补位

行记录的头信息

头信息中主要包含了6个字段,其中5个字段也是在面试中经常被问到的,为了方便记忆,我们把5个字段对应到手的5根指头:

  • n_owned(拇指): 一个数据页会被分成很多个组,每组最后的一条记录该字段为1,其他记录该字段为0,就像分组中所有的记录的大哥;(对应拇指)

  • deleted_flag(食指): 标记该记录是被删除的;当记录被删除时不会真实删除,而是用该字段标记,并且把所有删除的记录使用链表连接起来,以后的文章会继续说到这个字段。(想象下你平时挖鼻屎是不是用的食指)

  • heap_no(中指): 表示当前记录在数据页中的相对位置(MySQL使用该字段来表示记录位置,可以和中指对应,不可描述)

  • record_type(无名指): 表示当前记录属于哪种类型,(无名指用来带戒指的,与分类有关,可以把人分为已婚和未婚,)

    1. 0表示普通记录
    2. 1表示目录项记录,索引中非叶子结点中的数据记录都是1
    3. 2表示infrmum记录,每个数据页中至少会有两条记录,其中最小记录的record_type=2
    4. 3表示Supremum记录,每个数据页中至少会有两条记录,其中最大记录的record_type=3
  • next_record(小拇指): 存放下一条记录的相对位置(当数数时,左手的小拇指数完之后就该换右手了,和next_record表达的意思类型)

最后一个字段min_rec_flag : B+树中每层非叶子结点最小目录项记录该字段为1;该字段相对于其他5个字段显得不那么重要,不会影响理解B+树索引

隐藏列

除了用户自定义的数据列以外,MySQL还会为每行记录生成3个隐藏列

  • row_id: 行ID,记录的唯一标识;当用户在表中定义了主键字段就优先选择用户定义的主键,如果没有,就查找是否有定义不为null的唯一索引,如果有就把该列作为主键,如果没有MySQL就会生成一列row_id隐藏列作为主键
  • trx_id: 事务的ID;该字段对于实现一致性视图和事务隔离级别至关重要,以后会详细说明
  • roll_pointer: 回滚指针,指向的是该记录的上一个版本号,MySQL的MVCC主要就是通过这个字段来实现的。

溢出列

MySQL中所有的行记录都会被存储在数据页中,每个数据页的大小是16KB,也就是16384个字节;在前面我们讲过变长字段的长度可以用两个字节来表示,所以列的最大长度可以是65535,当遇到这种极端情况时,一个数据页是存储不下这一条记录的。

Compact行格式针对这种情况的处理方式是在真实的数据处记录该列的一部分数据(768字节),其他多余的数据会存储到新的数据页中(溢出页),然后在该记录中使用20个字节存储这些数据页的地址

溢出页与溢出页之间使用的链表相连接

其他的行记录格式:

Redundant:MySQL5.0之前的格式,直接忽略

Dynamic,CompressedCompact很像,只是在溢出列的处理有些差异,他们只会在真实数据列中使用20个字节存储溢出页的地址

面试题

  • char(M)定义的字段,在变长字段的长度列表中会记录该字段的长度吗?

欢迎大家在评论区留言讨论

优质文章推荐