mysql 的一行记录,最终肯定是存储在磁盘上,也就是肉眼可见的文件上,今天我们的目标很简单,就是看看它到底是怎么存的。
当然还有一个更重要的目标,就是以这个为引子,带大家完全通过一手资料,来揭秘这个问题的答案。
所以大家阅读时,不要完全奔着寻找这个答案去,如果很少通过一手资料去研究问题,那么相信完整读过本文,你会有收获和启发的。
我们先准备好三样东西。
1. 准备好一个 mysql 5.7 并将其启动。
2. 准备好 mysql 的官方文档放在旁边:
3. 准备好 mysql 的源码mysql截取,万一要用呢,别怕:
一手资料,就是官方文档 +源码 +二进制文件,其中二进制文件是我们自己去磁盘中找的,一会就知道了。
Let’s Go!
mysql 会把文件存在哪里呢?先找到他。
mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+---------------------------------------------+
| Variable_name | Value |
+---------------+---------------------------------------------+
| datadir | C:ProgramDataMySQLMySQL Server 5.7Data |
+---------------+---------------------------------------------+
1 row in set, 1 warning (0.00 sec)
我是 windows,就在这里了,进入这个目录。
这些是啥先不管,盯着它看就好了,我们继续。
第一步:创建数据库
mysql> create database flash;
盯着刚刚的文件夹看,此时会多出一个文件夹
同时这个文件夹里会多出一个文件,叫:
|-- flash
|-- db.opt
|--flash
|--performance_schema
看一眼它里面的内容,就知道他是干嘛的了。
default-character-set=latin1
default-collation=latin1_swedish_cidefault-character-set 是默认字符集,default-collation 是默认字符序。
字符集大家都了解,就不展开了。
字符序就是字符的排序和比较规则,一般以 _ci 结尾的表示大小写不敏感,_cs 结尾的表示大小写敏感,_bin 结尾的表示用编码值进行比较。
含义知道了,那我们重新设置它应该会有所变化,我们把这个数据库设置为开发时常用的 utf8mb4 格式。
ALTER SCHEMA `flash` DEFAULT CHARACTER SET utf8mb4;
再看 db.opt 文件,内容已经发生了变化。
default-character-set=utf8mb4
default-collation=utf8mb4_general_ciOK,那我们现在对这个文件有了个初步认识,创建一个新的数据库时,首先会多出一个以数据库名为名称的文件夹,然后文件夹里面会多出一个描述数据库配置的 db.opt 文件,我们继续!
第二步:创建表
创建一张 student 表,三列,其中 id 是主键。
CREATE TABLE `flash`.`student` (
`id` INT NOT NULL,
`name` VARCHAR(10) NOT NULL,
`age` INT NULL,
PRIMARY KEY (`id`)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4;此时 flash 文件夹中,多出了两个文件
|-- flash
|--db.opt
|--student.frm
|--student.ibd
|--flash
|--performance_schema
为了严谨,我们先看下 db.opt 文件有没有变化,发现没有任何变化,说明创建表对这个 db.opt 配置信息文件,没有影响。
再点开 student.frm,坏了,乱码了。
说明这个文件不是文本文件,用二进制方式打开它。
我把一些关键的地方都标上了含义,那这个文件的作用大家就一目了然了mysql截取,就是记录表结构嘛,具体的格式可以看 frm 文件结构的官方文档(写得太复杂了...我反正是没看):
db.opt 记录了数据库信息,student.frm 记录了表结构信息,那重头戏自然就在那个 student.ibd 文件了,这里一定存着具体的数据呀,索引呀等信息吧。
打开它!
果不其然还是乱码,那还是二进制打开它!截取了中间信息较为丰富的某部分。
发现一点也看不懂。
第三步:插入数据
我们加几条数据看看。
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('1', 'dibingfa2', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('2', 'dibingfa2', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('3', 'dibingfa3', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('4', 'dibingfa4', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('5', 'dibingfa5', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('6', 'dibingfa6', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('7', 'dibingfa7', '2');再二进制打开它! 发现有些东西我们可以看出点端倪了!猜测下这部分就是每一行的记录信息吧。
我们插入了七条数据,我发现这些二进制串有一段可以分割成七对,我把他单独拿出来,并且按行分割。
我们将第一行记录拆解,第一行记录的表数据是这样的。
1 dibingfa 2
在 ibd 文件中是这样的。
0800000010002480000001000000000A07A70000011B0110646962696E67666180000002
这串数据代表啥意思呢?由于本文只能参考官方文档,我们看这里,即 Innodb 行格式。
看这部分(我们的行格式是 DYNAMIC 类型,不过参考 COMPACT 类型描述也行,因为几乎一样,之后再说):
不要看这么一大长串就害怕,我们一点点来,别着急。
这个文档,我可能没资格评价,但我个人觉得写的很烂,一大堆废话也没说明白格式是什么样子,每个字节表示什么。不过也可能作用并非如此。
好的官方文档应该是能把每个字节和每一位都解释清楚的,无奈这个文档不行,那我们就去找更接近一手资料的源码。
我找到了源码,还是很清晰的,注释上就写明了每一行记录的磁盘数据格式,太好了,不用看代码了。
再贴上刚刚的第一行记录。
0800000010002480000001000000000A07A70000011B0110646962696E67666180000002
来一点点看,第一部分。
|lengthofthelastnon-nullvariable-lengthfieldofdata ... ...|
...
|lengthoffirstvariable-lengthfieldofdata|
这部分是变长字段长度列表,就是依次记录所有变长字段的长度,由于我们只有一个变长字段 varchar(10) 的 name,所以就是 08,我们存储的 "dibingfa" 刚好是 8 个字节,对上了。
那如果是多个,很显然,就这样存。
错!应该是这样存,也就是逆序存放,具体为啥后面说。
OK,这就是第一个字节 08 所表示的含义。
再往下。
|SQL-nullflags(1bitpernullablefield),paddedtofullbytes|
第二个结构叫 NUll 值列表,用 1 位表示一个 NULL 值,要填充满一个字节,那往下的一个字节是 00,一看我们的记录中也确实没有 NULL 值,对上了。
具体来说,同样也是逆序存放的。
继续。
|4bitsusedtodeletemarkarecord,andmarkapredefined
minimumrecordinalphabeticalorder|
|4bitsgivingthenumberofrecordsownedbythisrecord
(thistermisexplainedinpage0page.h)|
|13bitsgivingtheordernumberofthisrecordinthe
heapoftheindexpage|
|3bitsrecordtype:000=conventional,001=nodepointer(insideB-tree),
010=infimum,011=supremum,1xx=reserved|
|twobytesgivingarelativepointertothenextrecordinthepage|
ORIGINoftherecord
这五个字节很乱,放在一块叫记录头信息,00 00 10 00 24,其表示删除状态,记录类型,下一条记录的相对位置等。
这一大坨先放一放,因为涉及到好多额外的知识。
继续往下看。
|firstfieldofdata|
...
|lastfieldofdata|
剩下全都是具体的列数据了,从第一列到最后一列。
第一列是 ID 列,是 INT 类型的 1,占四个字节 80 00 00 01。开头的 80 是因为,正数要以 1 开头,这是 mysql 规定的,0x80 的二进制就是 1000 0000,所以这也对上了。
第二列是 name 列,是"dibingfa" 这样一个 varchar 类型的字符串。
可是与后面怎么也对应不上,这是咋回事呢?
还记不记得,mysql 每行记录会有几个隐藏列,rowid,事务 ID,回滚指针?没错,就是他们。
其中,因为有主键,所以 rowid 就不存在了,也可以说第一列要么是 mysql 为我们生成的 6 字节的 rowid,要么是用户定义的主键或其他 Unique 键,优先以用户定义的键为准。
下面我们一块看一下这五个列。(三个隐藏列,两个我们定义的列)
主键 ID:80 00 00 01
事务 ID:00 00 00 00 0A 07
回滚指针:A7 00 00 01 1B 01 10
name 列(dibingfa):64 69 62 69 6E 67 66 61
age 列(2):80 00 00 02
其中 age 列同刚刚说的一样,mysql 会为正数的前面,加一个 1,所以 age 为 2,在磁盘上存储的就是 80 00 00 02。
事务 ID 和回滚指针就涉及到事务、隔离级别和 MVCC 这一大坨八股文的知识点,这里不做展开。
行记录格式整体结构
总结下,整个一行记录的格式,叫做 mysql 的行记录格式,ROW_FORMAT。
这个 ROW_FORMAT 可以有不同的值,代表存储这一行记录的不同数据结构,其枚举记录在 remOtypes.h 文件中。
/** Innodb row types are a subset of the MySQL global enum row_type.
They are made into their own enum so that switch statements can account
for each of them. */
enum rec_format_enum {
REC_FORMAT_REDUNDANT = 0, /*!< REDUNDANT row format */
REC_FORMAT_COMPACT = 1, /*!< COMPACT row format */
REC_FORMAT_COMPRESSED = 2, /*!< COMPRESSED row format */
REC_FORMAT_DYNAMIC = 3 /*!< DYNAMIC row format */
};我电脑上用的是 mysql 5.7,其默认的行记录格式是DYNAMIC,这个在源码中也可以找到答案,在 ha_innodb.cc 中。
static ulong innodb_default_row_format = DEFAULT_ROW_FORMAT_DYNAMIC;
当然,可以用如下命令查询你的行格式。
show table status from flash like 'student';
所以我们今天以上讲述的格式,都是 DYNAMIC 格式的结构,总结起来如下:
记录源信息
变长字段列表
NULL 值列表
记录头信息
具体记录的各列信息
rowid 或主键(隐藏列)
事务 ID(隐藏列)
回滚指针(隐藏列)
列 1
列 2
...
列 n
刚刚那七条记录,整体分析下,就如下图。
然后多个行,一次紧密地排列,通过记录头中的下一条记录的相对位置指针信息,可以快速找到下一条记录的起始位置。
再宏观一点看,整个 ibd 文件,划分了很多个块,每个块 16 KB,我们这几行记录信息,在第四个块的某个区域内。
具体为什么是这个区域呢?因为这里是用户记录部分,前面还有文件头、页面头等信息,共占用 120 个字节,咱们今天讲的行记录部分,就从第 121 个字节开始。
慢慢的,我再和大家一起把其他部分搞清楚,那 mysql 这块我们就从最原始的磁盘数据入手,将 data 文件夹下的所有文件都搞清楚了。
最原始的数据都搞清楚了,原理还担心么?
再聊几句
其实,不要被行记录格式这种名词吓到,它只是个协议或规定罢了。
就是 mysql 规定了一种将一行记录存储在磁盘中的格式,以便于 mysql 自己的程序可以根据这个结构认识这一行记录。
所以这种协议,首先要满足让 mysql知道全部想知道的信息,比如 mysql 现在能仅仅通过 ibd 文件里的这些二进制数,知道每个字段的值都是什么吗?不能,因为它不知道表结构是什么样子,也就没法知道两个字段值之间的界限在哪里。
所以不难想到,它一定利用了 frm 文件中存储的表结构信息。
其次,要让 mysql 在知道这些信息的同时,还能更方便地利用这个结构,占用更少的存储空间,以及提升程序的便利性。
拿占用更少存储空间这块来讲,NULL 值完全可以当做普通列,也存储在后面,然后规定一个 NULL 值的二进制标识符即可。但 DYNAMIC 行记录格式规定前面放一个 NULL 值列表的结构,并且仅仅用 1 位来表示一个 NULL 值记录,这样就极大节省了空间。
再说便利性这块,上面说了变长字段长度列表和 NULL 值列表,都是逆序存储的,看似很别扭,其实就是为了程序的便利性,这里留给大家自己探索吧。
文章附赠
恭喜读到了这里,文章附赠一份小礼物,就是本文输出的一些文件。
大家加我好友(公众号菜单栏中有个联系我),看朋友圈第一条,即可获得这些文件的下载路径。
哦对了,顺便提一下,记得很久之前还是用 sqlyog 去连 mysql 的,其实 mysql 下载并安装后,自带的 workbench 就很好用了。
所以非常方便本机做实验。
大家方便的时候也自己玩一玩哦,我们下期见。
限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。