mysql 的一行记录,最终肯定是存储在磁盘上,也就是肉眼可见的文件上,今天我们的目标很简单,就是看看它到底是怎么存的。当然还有一个更重要的目标,就是以这个为引子,带大家完全通过一手资料,来揭秘这个问题的答案。所以大家阅读时,不要完全奔着寻找这个答案去,如果很少通过一手资料去研究问题,那么相信完整读过本文,你会有收获和启发的。1. 准备好一个 mysql 5.7 并将其启动。2. 准备好 mysql 的官方文档放在旁边:
https://dev.mysql.com3. 准备好 mysql 的源码,万一要用呢,别怕:
https://dev.mysql.com/downloads/mysql/5.7.html一手资料,就是官方文档 + 源码 + 二进制文件,其中二进制文件是我们自己去磁盘中找的,一会就知道了。mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+---------------------------------------------+
| Variable_name | Value |
+---------------+---------------------------------------------+
| datadir | C:\ProgramData\MySQL\MySQL Server 5.7\Data\ |
+---------------+---------------------------------------------+
1 row in set, 1 warning (0.00 sec)
复制
mysql> create database flash;
复制
|-- flash
|-- db.opt
|-- flash
|-- performance_schema
default-character-set=latin1
default-collation=latin1_swedish_ci
复制
default-character-set 是默认字符集,default-collation 是默认字符序。字符序就是字符的排序和比较规则,一般以 _ci 结尾的表示大小写不敏感,_cs 结尾的表示大小写敏感,_bin 结尾的表示用编码值进行比较。含义知道了,那我们重新设置它应该会有所变化,我们把这个数据库设置为开发时常用的 utf8mb4 格式。ALTER SCHEMA `flash` DEFAULT CHARACTER SET utf8mb4;
复制
default-character-set=utf8mb4
default-collation=utf8mb4_general_ci
复制
OK,那我们现在对这个文件有了个初步认识,创建一个新的数据库时,首先会多出一个以数据库名为名称的文件夹,然后文件夹里面会多出一个描述数据库配置的 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
|-- db.opt
|-- student.frm
|-- student.ibd
|-- flash
|-- performance_schema为了严谨,我们先看下 db.opt 文件有没有变化,发现没有任何变化,说明创建表对这个 db.opt 配置信息文件,没有影响。
我把一些关键的地方都标上了含义,那这个文件的作用大家就一目了然了,就是记录表结构嘛,具体的格式可以看 frm 文件结构的官方文档(写得太复杂了...我反正是没看):https://dev.mysql.com/doc/internals/en/frm-file-format.htmldb.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');
复制
复制
发现有些东西我们可以看出点端倪了!猜测下这部分就是每一行的记录信息吧。我们插入了七条数据,我发现这些二进制串有一段可以分割成七对,我把他单独拿出来,并且按行分割。我们将第一行记录拆解,第一行记录的表数据是这样的。08 00 00 00 10 00 24 80 00 00 01 00 00 00 00 0A 07 A7 00 00 01 1B 01 10 64 69 62 69 6E 67 66 61 80 00 00 02这串数据代表啥意思呢?由于本文只能参考官方文档,我们看这里,即 Innodb 行格式。https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html看这部分(我们的行格式是 DYNAMIC 类型,不过参考 COMPACT 类型描述也行,因为几乎一样,之后再说):这个文档,我可能没资格评价,但我个人觉得写的很烂,一大堆废话也没说明白格式是什么样子,每个字节表示什么。不过也可能作用并非如此。好的官方文档应该是能把每个字节和每一位都解释清楚的,无奈这个文档不行,那我们就去找更接近一手资料的源码。我找到了源码,还是很清晰的,注释上就写明了每一行记录的磁盘数据格式,太好了,不用看代码了。08 00 00 00 10 00 24 80 00 00 01 00 00 00 00 0A 07 A7 00 00 01 1B 01 10 64 69 62 69 6E 67 66 61 80 00 00 02| length of the last non-null variable-length field of data ... ...|
...
| length of first variable-length field of data |
这部分是变长字段长度列表,就是依次记录所有变长字段的长度,由于我们只有一个变长字段 varchar(10) 的 name,所以就是 08,我们存储的 "dibingfa" 刚好是 8 个字节,对上了。错!应该是这样存,也就是逆序存放,具体为啥后面说。| SQL-null flags (1 bit per nullable field), padded to full bytes |第二个结构叫 NUll 值列表,用 1 位表示一个 NULL 值,要填充满一个字节,那往下的一个字节是 00,一看我们的记录中也确实没有 NULL 值,对上了。| 4 bits used to delete mark a record, and mark a predefined
minimum record in alphabetical order |
| 4 bits giving the number of records owned by this record
(this term is explained in page0page.h) |
| 13 bits giving the order number of this record in the
heap of the index page |
| 3 bits record type: 000=conventional, 001=node pointer (inside B-tree),
010=infimum, 011=supremum, 1xx=reserved |
| two bytes giving a relative pointer to the next record in the page |
ORIGIN of the record
这五个字节很乱,放在一块叫记录头信息,00 00 10 00 24,其表示删除状态,记录类型,下一条记录的相对位置等。| first field of data |
...
| last field of data |
第一列是 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 值列表
记录头信息

然后多个行,一次紧密地排列,通过记录头中的下一条记录的相对位置指针信息,可以快速找到下一条记录的起始位置。再宏观一点看,整个 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 就很好用了。