一文带你了解MySQL之InnoDB 记录结构【上篇】

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 到现在为⽌,MySQL对于我们来说还是⼀个⿊盒,我们只负责使⽤客户端发送请求并等待服务器返回结果,表中的数据到底存到了哪⾥?以什么格式存放的?MySQL是以什么⽅式来访问的这些数据?这些问题我们统统不知道?之前的文章我们大致知道MySQL服务器上负责对表中数据的读取和写⼊⼯作的部分是存储引擎,⽽服务器⼜⽀持不同类型的存储引擎,⽐如InnoDB、MyISAM、Memory等,不同的存储引擎⼀般是由不同的⼈为实现不同的特性⽽开发的,真实数据在不同存储引擎中存放的格式⼀般是不同的,甚⾄有的存储引擎⽐如Memory都不⽤磁盘来存储数据,也就是说关闭服务器后表中的数据就消失了。

前言


到现在为⽌,MySQL对于我们来说还是⼀个⿊盒,我们只负责使⽤客户端发送请求并等待服务器返回结果,表中的数据到底存到了哪⾥?以什么格式存放的?MySQL是以什么⽅式来访问的这些数据?这些问题我们统统不知道?之前的文章我们大致知道MySQL服务器上负责对表中数据的读取和写⼊⼯作的部分是存储引擎,⽽服务器⼜⽀持不同类型的存储引擎,⽐如InnoDB、MyISAM、Memory等,不同的存储引擎⼀般是由不同的⼈为实现不同的特性⽽开发的,真实数据在不同存储引擎中存放的格式⼀般是不同的,甚⾄有的存储引擎⽐如Memory都不⽤磁盘来存储数据,也就是说关闭服务器后表中的数据就消失了。由于InnoDB是MySQL默认的存储引擎,也是我们最常⽤到的存储引擎,所以本文带你了解InnoDB存储结构。当我们熟悉了⼀个存储引擎的数据存储结构之后,其他的存储引擎都是依葫芦画瓢。


一、InnoDB⻚简介

InnoDB是⼀个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。⽽真正处理数据的过程是发⽣在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写⼊或修改请求的话,还需要把内存中的内容刷新到磁盘上。⽽我们知道读写磁盘的速度⾮常慢,和内存读写差了⼏个数量级,所以当我们想从表中获取某些记录时,InnoDB采取的⽅式是:将数据划分为若⼲个⻚,以⻚作为磁盘和内存之间交互的基本单位,InnoDB中⻚的⼤⼩⼀般为 16K,也就是在⼀般情况下,⼀次最少从磁盘中读取16KB的内容到内存中,⼀次最少把内存中的16KB内容刷新到磁盘中。


二、InnoDB⾏格式

我们平时是以记录为单位来向表中插⼊数据的,这些记录在磁盘上的存放⽅式也被称为⾏格式或者记录格式。设计InnoDB存储引擎到现在为⽌设计了4种不同类型的⾏格式,分别是:

Compact

Redundant

Dynamic

Compressed

随着时间的推移,他们可能会设计出更多的⾏格式,但是不管怎么变,在原理上⼤体都是差不多


三、指定行格式的语法

create table表名 (列的信息) row_format=⾏格式名称

or

alter table 表名 row_format=⾏格式名称

比如我们在testdb库中创建一张学习的表demo,可以这样指定它的行格式

mysql> use testdb;

Database changed

mysql> create table demo1( c1 varchar(10), c2 varchar(10) not null, c3 char(10), c4 varchar(10), c5 varchar(1024)) charset=ascii row_format=compact;

Query OK, 0 rows affected (0.01 sec)


可以看到我们刚刚创建的这个表的⾏格式就是compact,我们还指定了这个表的字符集为ascii,因为ascii字符集只包括空格、标点符号、数字、⼤⼩写字⺟和⼀些不可⻅字符。我们现在向这个表中插⼊两条记录


mysql> insert into demo1 values('aaaaa','bbbb','ccc','dd','e');

Query OK, 1 row affected (0.00 sec)

mysql> insert into demo1 values('eeeee','ffff',null,null,'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz');

Query OK, 1 row affected (0.01 sec)

现在表中的记录就是这个样⼦的


mysql> select * from demo1;

+-------+------+------+------+------------------------------------------------------------------------------------------------------------------------------------+

| c1    | c2   | c3   | c4   | c5                                                                                                                                 |

+-------+------+------+------+------------------------------------------------------------------------------------------------------------------------------------+

| aaaaa | bbbb | ccc  | dd   | e                                                                                                                                  |

| eeeee | ffff | NULL | NULL | abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz |

+-------+------+------+------+------------------------------------------------------------------------------------------------------------------------------------+

2 rows in set (0.00 sec)


演示表的内容也填充好了,现在我们就来看看各个⾏格式下的存储⽅式到底有啥不同吧


四、Compact⾏格式


如图,⼀条完整的记录其实可以被分为记录的额外信息和记录的真实数据两⼤部分,下边我们详细看⼀下这两部分的组成

微信图片_20230525224742.png

4.1 记录的额外信息

这部分信息是服务器为了描述这条记录添加的⼀些信息,这些额外信息分为3类,分别是变⻓字段⻓度列表、NULL值列表和记录头信息,我们分别看⼀下


4.1.1 变⻓字段⻓度列表

我们知道MySQL⽀持⼀些变⻓的数据类型,⽐如varchar(M)、varbinary(M)、各种TEXT类型,各种BLOB类型,我们也可以把拥有这些数据类型的列称为变⻓字段,变⻓字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占⽤的字节数也存起来,这样才不⾄于把MySQL服务器搞懵,所以这些变⻓字段占⽤的存储空间分为两部分:


真正的数据内容

占⽤的字节数

在Compact⾏格式中,把所有变⻓字段的真实数据占⽤的字节⻓度都存放在记录的开头部位,从⽽形成⼀个变⻓字段⻓度列表,各变⻓字段数据占⽤的字节数按照列的顺序逆序存放。


我们拿demo1表中的第⼀条记录来举个例⼦。因为demo1表的、c1、c2、c4、c5列都是变⻓的数据类型,所以这四个列的值的字节⻓度都需要保存在记录开头处,我们来看⼀下第⼀条记录各变⻓字段内容字节的⻓度:


列名 存储内容 字节长度(十进制) 内容长度(十六进制)

c1 ‘aaaaa’ 5 0x05

c2 ‘bbbb’ 4 0x04

c4 ‘dd’ 2 0x02

c5 ‘e’ 1 0x01

⼜因为这些⻓度值需要按照列的逆序存放,所以最后变⻓字段⻓度列表的字节串⽤⼗六进制表示,效果就是:01 02 04 05,把这个字节串组成的变⻓字段⻓度列表填⼊上边的示意图中的效果如下:

微信图片_20230525224835.png


我们也可以查看底层存储文件:demo1.ibd,用16进制编辑器打开,我这里使用的是Notepad++和他的HEX-Editor插件。可以找到如下的数据域(可能会有其中 mysql 生成的行数据不一样,但是我们创建的行数据内容应该是一样的,而且数据长度应该是一摸一样的,可以搜索01 02 04 05字符,找到这些数据):

微信图片_20230525224858.png


由于第⼀⾏内容占⽤的字节数⽐较⼩,⽤1个字节就可以表示,但是如果变⻓列的内容占⽤的字节数⽐较多,可能就需要⽤2个字节来表示。具体⽤1个还是2个字节来表示真实数据占⽤的数,InnoDB有它的⼀套规则,这里我们⾸先声明⼀下W、M和L的意思:


假设某个字符集中表示⼀个字符最多需要使⽤的字节数为W


也就是使⽤show charset语句的结果中的Maxlen列,⽐⽅说utf8mb4字符集中W就是4,utf8字符集中的W就是3,gbk字符集中的W就是2,ascii字符集中的W就是1


对于变⻓类型varchar(M)来说,这种类型表示能存储最多M个字符,所以这个类型能表示的字符串最多占⽤的字节数就是M×W。


假设它实际存储的字符串占⽤的字节数是L


所以确定使⽤1个字节还是2个字节表示真正字符串占⽤的字节数的规则就是这样:


如果M×W <= 255,那么使⽤1个字节来表示真正字符串占⽤的字节数


如果M×W > 255,则分为两种情况:


如果L <= 127,则⽤1个字节来表示真正字符串占⽤的字节数

如果L > 127,则⽤2个字节来表示真正字符串占⽤的字节数

也就是如果该可变字段允许存储的最⼤字节数(M×W)超过255字节并且真实存储的字节数(L)超过127字节,则使⽤2个字节,否则使⽤1个字节。

另外需要注意的⼀点是,变⻓字段⻓度列表中只存储值为⾮NULL 的列内容占⽤的⻓度,值为 NULL的列的⻓度是不储存的


我们查看第二行数据,第二行数据c5,它的字符串最多占⽤的字节数是1024,实际存储的字符串占用的字节是130,所以要用两个字节来表示长度,按照逆序存放 就是

微信图片_20230525224915.png

所以两行数据示意图如下:

微信图片_20230525224954.png



4.1.2 NULL值列表

我们知道表中的某些列可能存储NULL值,如果把这些NULL值都放到记录的真实数据中存储会很占地⽅,所以Compact⾏格式把这些值为NULL的列统⼀管理起来,存储到NULL值列表中,它的处理过程是这样的


⾸先统计表中允许存储NULL的列有哪些

我们前边说过,主键列、被NOT NULL修饰的列都是不可以存储NULL值的,所以在统计的时候不会把这些列算进去。⽐⽅说表demo1的4个列c1、c3、c4、c5 都是允许存储NULL值的,⽽c2列是被NOT NULL修饰,不允许存储NULL值。


如果表中没有允许存储 NULL 的列,则 NULL值列表也不存在了,否则将每个允许存储NULL的列对应⼀个⼆进制位,⼆进制位按照列的顺序逆序排列,⼆进制位表示的意义如下


⼆进制位的值为1时,代表该列的值为NULL。

⼆进制位的值为0时,代表该列的值不为NULL

因为表demo1有4个值允许为NULL的列,所以这4个列和⼆进制位的对应关系就是这样:

微信图片_20230525225015.png

MySQL规定NULL值列表必须⽤整数个字节的位表示,如果使⽤的⼆进制位个数不是整数个字节,则在字节的⾼位补0。

对于第⼀条记录来说,c1、c3、c4、c5 这4个列的值都不为NULL,所以它们对应的个二进制位,不足一个字节,所以在字节的高位补0,效果就是这样:

微信图片_20230525225039.png

所以第⼀条记录的NULL值列表⽤⼗六进制表示就是:0x00,以此类推,如果一个表中有9个允许为NULL,那这个记录的NULL值列表部分就需要2个字节来表示了

微信图片_20230525225059.png


第二条数据c3、c4为值都为NULL,所以这4个列对应的⼆进制位的情况就是:

微信图片_20230525225135.png



所以第⼆条记录的NULL值列表⽤⼗六进制表示就是:0x06,查看文件

微信图片_20230525225115.png


所以这两条记录在填充了NULL值列表后的示意图就是这样:




4.1.3 记录头信息

除了变⻓字段⻓度列表、NULL值列表之外,还有⼀个⽤于描述记录的记录头信息,它是由固定的5个字节组成。5个字节也就是40个⼆进制位,不同的位代表不同的意思,如图:

微信图片_20230525225209.png

这些⼆进制位代表的详细信息如下表:


名称 ⼤⼩(单位: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 表示下⼀条记录的相对位置

我们查看demo1.ibd存储文件:

微信图片_20230525225226.png


根据推理,很容易可以得到demo1数据表两条记录的记录头信息如下所示


第一行:00 00 10 00 3a

第二行:00 00 18 ff bb


根据记录头的构成,将上面的记录由十六进制转换为二进制分析

第一行:00000000 00000000 00010000 00000000 00110111

第二行:00000000 00000000 00011000 11111111 10111011


根据这些二进制数据,将数据按照记录头结构切分可以得到如下信息


预留位1(1b) 预留位2(1b) delete_mask(1b) min_rec_mask(1b) n_owned(4b) heap_no(13) record_type(3b) next_record(16b)

第一行 0 0 0 0 0000 00000000 00010(2) 000 00000000 00110111(55)

第二行 0 0 0 0 0000 00000000 00011(3) 000 11111111 10111011(-69)

第二行最后16个字节11111111 10111011是负数,补码+1,1000101,十进制就是-69


所以这两条记录在填充了记录头信息后的示意图就是这样:

微信图片_20230525225248.png

4.2 记录的真实数据

对于demo1表来说,记录的真实数据除了c1、c2、c3、c4、c5这⼏个我们⾃⼰定义的列的数据以外,MySQL会为每个记录默认的添加⼀些列(也称为隐藏列),具体的列如下:


列名 是否必须 占⽤空间 描述

row_id 否 6字节 ⾏ID,唯⼀标识⼀条记录

transaction_id 是 6字节 事务ID

roll_pointer 是 7字节 回滚指针

小提示:

实际上这⼏个列的真正名称其实是:DB_ROW_ID DB_TRX_ID、DB_ROLL_PTR,为了美观才写成了row_id、transaction_id和roll_pointer,这⾥需要提⼀下InnoDB表对主键的⽣成策略:优先使⽤⽤户⾃定义主键作为主键,如果⽤户没有定义主键,则选取⼀个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加⼀个名为row_id的隐藏列作为主键。所以我们从上表中可以看出:InnoDB存储引擎会为每条记录都添加DB_TRX_ID和DB_ROLL_PTR这两个列,但是row_id是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)。这些隐藏列的值不用我们操心,InnoDB存储引擎会自己帮我们生成的。


因为表demo1并没有定义主键,所以MySQL服务器会为每条记录增加上述的3个列。

微信图片_20230525225326.png


我们看看第一行内容的文件:

微信图片_20230525225338.png


我们给第一行记录加上的真实数据,如下

微信图片_20230525225351.png


参考ASCII码对照表:16进制的61对应a 62对应b 63对应c 20对应空格(char以外的7个字节的统统都用空格字符填充) 64对应d 65对应e,懵逼了吧


4.3 CHAR(M)列的存储格式

demo1表的c1、c2、c4、c5列的类型是VARCHAR(10),而c3列的类型是CHAR(10),demo1表采用的是ascii字符集,这个字符集是一个定长字符集,也就是说表示一个字符采用固定的一个字节,如果采用变长的字符集(也就是表示一个字符需要的字节数不确定,比如gbk表示一个字符要1~2个字节、utf8表示一个字符要1~3个字节等)的话,c3列的长度也会被存储到变长字段长度列表中。


对于CHAR(M)类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。


小提示:

变长字符集的CHAR(M)类型的列要求至少占用M个字节,而VARCHAR(M)却没有这个要求。比方说对于使用utf8字符集的CHAR(10)的列来说,该列存储的数据字节长度的范围是10~30个字节。即使我们向该列中存储一个空字符串也会占用10个字节,这是怕将来更新该列的值的字节长度大于原有值的字节长度而小于10个字节时,可以在该记录处直接更新,而不是在存储空间中重新分配一个新的记录空间,导致原有的记录空间成为所谓的碎片。


我们创建一张demo2表来看一下:

mysql> create table demo2( c1 varchar(10), c2 varchar(10) not null, c3 char(10), c4 varchar(10), c5 varchar(1024)) charset=utf8 row_format=compact;

Query OK, 0 rows affected, 1 warning (0.03 sec)

mysql> insert into demo2 values('aaaaa','bbbb','ccc','dd','e');

Query OK, 1 row affected (0.01 sec)


这里就不唠叨了,直接打开demo2.ibd文件查看

微信图片_20230525225454.png


示意图中的效果如下:

微信图片_20230525225511.png

我们在插入一条数据


mysql> insert into demo2 values('一一一一一','一一 一','测试测试测试测测试','dd','e');


这条数据的字节数如下:5*3=15 3*3+1=10 3*9=27 2 1对应16进制逆序存放 01 02 1b oa of ,这里就不画图了,我们直接看demo2.ibd文件

微信图片_20230525225524.png


这篇文档到此结束了,后面在讲其他的行格式,行溢出。本篇概念性有点强,大家可以慢慢理解。


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
12天前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
本文介绍了MySQL InnoDB存储引擎中的数据文件和重做日志文件。数据文件包括`.ibd`和`ibdata`文件,用于存放InnoDB数据和索引。重做日志文件(redo log)确保数据的可靠性和事务的持久性,其大小和路径可由相关参数配置。文章还提供了视频讲解和示例代码。
119 11
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
|
12天前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的表空间
InnoDB是MySQL默认的存储引擎,主要由存储结构、内存结构和线程结构组成。其存储结构分为逻辑和物理两部分,逻辑存储结构包括表空间、段、区和页。表空间是InnoDB逻辑结构的最高层,所有数据都存放在其中。默认情况下,InnoDB有一个共享表空间ibdata1,用于存放撤销信息、系统事务信息等。启用参数`innodb_file_per_table`后,每张表的数据可以单独存放在一个表空间内,但撤销信息等仍存放在共享表空间中。
|
12天前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的段、区和页
MySQL的InnoDB存储引擎逻辑存储结构与Oracle相似,包括表空间、段、区和页。表空间由段和页组成,段包括数据段、索引段等。区是1MB的连续空间,页是16KB的最小物理存储单位。InnoDB是面向行的存储引擎,每个页最多可存放7992行记录。
|
12天前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL的InnoDB存储引擎
InnoDB是MySQL的默认存储引擎,广泛应用于互联网公司。它支持事务、行级锁、外键和高效处理大量数据。InnoDB的主要特性包括解决不可重复读和幻读问题、高并发度、B+树索引等。其存储结构分为逻辑和物理两部分,内存结构类似Oracle的SGA和PGA,线程结构包括主线程、I/O线程和其他辅助线程。
【赵渝强老师】MySQL的InnoDB存储引擎
|
11天前
|
SQL 关系型数据库 MySQL
12 PHP配置数据库MySQL
路老师分享了PHP操作MySQL数据库的方法,包括安装并连接MySQL服务器、选择数据库、执行SQL语句(如插入、更新、删除和查询),以及将结果集返回到数组。通过具体示例代码,详细介绍了每一步的操作流程,帮助读者快速入门PHP与MySQL的交互。
26 1
|
13天前
|
SQL 关系型数据库 MySQL
go语言数据库中mysql驱动安装
【11月更文挑战第2天】
29 4
|
1月前
|
存储 关系型数据库 MySQL
Mysql(4)—数据库索引
数据库索引是用于提高数据检索效率的数据结构,类似于书籍中的索引。它允许用户快速找到数据,而无需扫描整个表。MySQL中的索引可以显著提升查询速度,使数据库操作更加高效。索引的发展经历了从无索引、简单索引到B-树、哈希索引、位图索引、全文索引等多个阶段。
63 3
Mysql(4)—数据库索引
|
20天前
|
监控 关系型数据库 MySQL
数据库优化:MySQL索引策略与查询性能调优实战
【10月更文挑战第27天】本文深入探讨了MySQL的索引策略和查询性能调优技巧。通过介绍B-Tree索引、哈希索引和全文索引等不同类型,以及如何创建和维护索引,结合实战案例分析查询执行计划,帮助读者掌握提升查询性能的方法。定期优化索引和调整查询语句是提高数据库性能的关键。
96 1
|
22天前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据需求选择最合适的方法。通过具体案例,展示了编译源码安装的灵活性和定制性。
65 2
|
25天前
|
存储 关系型数据库 MySQL
MySQL vs. PostgreSQL:选择适合你的开源数据库
在众多开源数据库中,MySQL和PostgreSQL无疑是最受欢迎的两个。它们都有着强大的功能、广泛的社区支持和丰富的生态系统。然而,它们在设计理念、性能特点、功能特性等方面存在着显著的差异。本文将从这三个方面对MySQL和PostgreSQL进行比较,以帮助您选择更适合您需求的开源数据库。
96 4
下一篇
无影云桌面