文章目录
1. 前言
对于MySQL来说,我们对它的认知并不多,只知道它的主要功能是存储数据。
但是 这些数据存放在哪里?以什么格式存放?MySQL怎么访问这些数据? 我们都不知道。
对于MySQL服务器来说,负责对表数据进行读取和写入工作的是存储引擎。在MySQL中有很多存储引擎,比如InnoDB、MyISAM等。不同的存储引擎存放数据的格式一般是不一样的。
对于MEMORY这个存储引擎来说,它不用磁盘存储数据,也就意味着服务器关闭后表的数据就消失了。
而InnoDB是MySQL的默认存储引擎,它是一个将表中数据存储在磁盘上的存储引擎。
2. InnoDB概述
InnoDB会将数据划分为若干个页,以页作为磁盘和内存之间的交互的基本单位。并且页的大小一般为16KB,也就是说,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中16KB内容刷新到磁盘中。这样的设计,使得磁盘和内存之间数据交互不那么慢。
在MySQL中,系统变量innodb_page_size
表示了InnoDB的存储引擎中的页的大小,默认值为16384字节,也就是16KB。但是需要注意的是,在MySQL服务器运行过程中不可以更改页面大小!!!!
3. InnoDB行格式
在InnoDB中,设计者设计了4种行格式
- COMPACT
- REDUNDANT
- DYNAMIC
- COMPRESSED
行格式,也称记录格式,是指记录在磁盘上的存放形式。
对于行格式,可以使用以下语法来指定行格式
CREATE TABLE 表名(列的信息) ROW_FORMAT=行格式名称;
ALTER TABLE 表名 ROW_FORMAT=行格式名称;
3.1 COMPACT 行格式
COMPACT行格式的示意图如下
由示意图可以看出,一条记录分为两大部分,分别是记录的额外信息和记录的真实数据
3.1.1 记录的额外信息
记录的额外信息包括三部分,分别是变长字段长度列表、NULL值列表和记录头信息。
什么是变长字段长度列表?
在MySQL中,有一些变长的数据类型,比如VARCHAR等。这些类型的列就可以被称为变长字段。变长字段存储的数据长度是不固定的,因此需要将这些数据占用的字节数存储起来。
在 COMPACT 行格式中,所有变长字段的真实数据占用的字节数都存放在记录的开头位置,从而形成一个变长字段长度列表,各变长字段的真实数据占用的字节数按照列的顺序逆序存放。
并且,并不是所有记录都有变长字段长度列表这部分,如果表中所有列都不是变长的数据类型或者所有列的值都是NULL的画,就不需要变长字段长度列表了。
对于这样的一个表
表的数据如下
+------+-----+------+------+
| c1 | c2 | c3 | c4 |
+------+-----+------+------+
| aaaa | bbb | cc | d |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
对于第一条数据来说,变长字段的内容长度如下
列名 | 存储内容 | 内容长度(十进制表示) | 内存长度(十六进制表示) |
---|---|---|---|
c1 | ‘aaaa’ | 4 | 0x04 |
c2 | ‘bbb’ | 3 | 0x03 |
c3 | ‘d’ | 1 | 0x01 |
因此,变长字段长度列表字节串用十六进制表示效果如下
01 03 04
#注意:这里实际上是没有空格的,只是为了方便展示
第一条记录的变长字段长度存储如上图所示。
**像上面的变长字段的内容占用的字节数用1字节就可以表示(**也就是内容长度4,3,1可以分别使用十六进制0x04、0x03、0x01表示),但是如果变长字段的内容占用字节比较多,就需要使用2字节来表示了。
什么时候使用1字节表示?什么时候使用2字节表示呢?
InnoDB有属于它的规则。
- 假如某个字符集最多需要W字节来表示一个字符。比如utf8mb4字符集中的W就是4,GBK字符集中的W就是2。
- 对于变长类型VARCHAR(M)来说,这种类型最多存储M个字符,所以这种类型表示的字符串最多占用字节为
M × W
- 假设该变长字段实际存储的字符串占用字节数为L
- 那么如果
M × W <= 255
,那么使用1字节来表示真实数据占用的字节数 - 如果
M × W > 255
- 如果
L <= 127
,则使用1字节来表示真实数据占用的字节数 - 如果
L > 127
,则使用2字节来表示真实数据占用的字节数
- 如果
什么是NULL值列表
NULL值列表是统一管理一条记录中值为NULL的列。
它的处理过程如下:
- 首先统计表中允许存储NULL的列有哪些。也就是排除被NOT NULL修饰的列。
- 如果表中没有允许存储NULL的列,则NULL值列表就不存在了。
- 否则,将每个允许存储NULL的列对应一个二进制,二进制位按照列的存储逆序排序
- 二进制位的值为1时,表示该列的值为NULL
- 二进制位的值为0时,表示该列的值不为NULL
- MySQL规定NULL值列表必须用整个字节的位表示,二进制位个数不是整个字节的需要补上0
因此,对于上面提到的这个表
+------+-----+------+------+
| c1 | c2 | c3 | c4 |
+------+-----+------+------+
| aaaa | bbb | cc | d |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
两条记录的NULL值列表如下
第二条记录的06的由来是这样的:
在表中,c2列被NOT NULL修饰,所以不可能为NULL,只有c1和c3和c4可能为NULL。
但在第二条记录中,只有c1不为NULL,因此倒序排列后的NULL列表为110
110转化为十进制就是06
什么是记录头信息?
记录头信息是由固定的5字节组成,用于描述记录的一些属性,5字节也就是40个二进制位。
记录头信息中的各二进制代表的详细信息如下图所示
3.1.2 记录真实的数据
这里说的记录真实的数据除了自己定义的列的数据外,MySQL还会默认给每条记录加一些列。
列名 | 是否必需 | 占用空间 | 描述 |
---|---|---|---|
row_id | 否 | 6字节 | 行ID,唯一标识一条记录 |
trx_id | 是 | 6字节 | 事务ID |
roll_pointer | 是 | 7字节 | 回滚指针 |
InnoDB表的主键生成策略:
- 优先使用用户自定义主键作为主键
- 如果没有自定义主键,那么就会选取一个不允许存储NULL值的UNIQUE键作为主键。
- 如果表中连不允许存储 NULL值的 UNIQUE 键都没有定义,,则InnoDB 会为表默认添加一个名为 row_id 隐藏列作为主键.
在上面提到的表中,数据如下
+------+-----+------+------+
| c1 | c2 | c3 | c4 |
+------+-----+------+------+
| aaaa | bbb | cc | d |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
- 这个表使用的是ascii字符集,因此0x61616161表示的就是字符串’aaaa’,以此类推
- 但是需要注意的是,c3列用的是CHAR(10)类型的,它存储的是’cc’,得到的结果是0x6363。CHAR是不可变的,虽然只占用了2字节,但是c3依然会占用10字节,其余8个字节用空格字符填充,也就是0x20
- 对于第二条记录的NULL值,它们被存储在前面的NULL值列表处,就不在记录真实数据存储了。
3.1.3 CHAR(M)列的存储格式
在表结构使用ascii字符集的时候,由于该字符集采用固定的一个字节来编码一个字符,是一个定长编码字符集,所以说CHAR不属于变长字段。
但是如果采用变长编码的字符集(如utf8表示1~3字节),即使使用的类型为CHAR,该类型对应的类也会存储到变长字段长度列表中!!!
在COMPACT 行格式中规定,采用变长编码字符集的CHAR(M)类型的列要求至少占用M个字节,意思就是说我们向列中存储一个空字符串也会占用10字节。
这样设计的意图是想以后更新该列的时候,如果新值大于旧值得字节长度但不大于10字节的时候,可以直接更新,而不用重新分配一个新的记录空间。
3.2 REDUNDANT 行格式
REDUNDANT 行格式的示意图如下
3.2.1 字段长度偏移列表
在REDUNDANT行格式中,没有了“变长”,意味着 REDUNDANT行格 会把该条记录中所有列(包括隐藏列)的长度信息按照逆序存储到字段长度偏移列表中
它的偏移量是通过采用两个相邻偏移量的差值来计算各个列值的长度的
继续以这个表的数据来说
+------+-----+------+------+
| c1 | c2 | c3 | c4 |
+------+-----+------+------+
| aaaa | bbb | cc | d |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
第一条记录的字段长度偏移列表为
25 24 1A 17 13 0C 06
顺序排序后
06 0C 13 17 1A 24 25
这个是怎么计算出来的?
- 第一列(row_id)的长度就是0x06个字节,也就是6字节
- 第二列(trx_id)的长度就是(0x0C - 0x06),也就是6个字节
- 第三列(roll_pointer)的长度就是(0x13 - 0x0C)个字节,也就是7个字节
其他列以此类推即可!!!
3.2.2 记录头信息
REDUNDANT 行格式的记录头信息占用6个字节,总计48个二进制位
第一条记录中的头信息是
00 00 10 0F 00 BC
根据此可以得出各属性信息
对比COMPACT 行格式可以发现,REDUNDANT 行格式对了n_field
和1byte_offs_flag
这两个属性
但是缺少了record_type
这个属性
1byte_offs_flag
的值决定了每个列的偏移量是使用1字节还是2字节
- 当它的值为1的时候,表示使用1字节来表示存储偏移量
- 当它的值为0的时候,表示使用2字节来表示存储偏移量。
那么
1byte_offs_flag
的值怎么选择的呢?
这是根据REDUNDANT行格式记录的真实数据占用的大小来判断的
- 当记录的真实数据占用的字节数不大于 127 (十六进制 0x7F,二进制 01111111)时,每个列对应的偏移量占用 1 字节。
- 当记录的真实数据占用的字节数大于127,但不大于32767(十六进制0x7FFF,二进制0111111111111111)时,每个列对应的偏移量占用2字节。
- 有没有记录的真实数据大于32767的情况呢?有,不过此时记录的一部分已经存放到了所谓的溢出页中(后面我们会详细讨论),在本页中只保留前 768字节和20字节的溢出页面地址(当然这20字节中还记录了一些别的信息)。在这种情况下只使用2字节来存储每个列对应的偏移量就够了。
3.2.3 NULL值处理
REDUNDANT行格式中将列对应的偏移量值得第一个比特位作为是否为NULL的依据,这个比特位也成为NULL比特位。
这就是为什么只要记录的真实数据大于 127 (十六进制0x7F,二进制01111111)时候,就采用2字节来表示一个列对应的偏移量了。
+------+-----+------+------+
| c1 | c2 | c3 | c4 |
+------+-----+------+------+
| aaaa | bbb | cc | d |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
对于这里的第二条记录,长度偏移列表为
A4 A4 1A 17 13 0C 06
顺序排序就是
06 C 13 17 1A A4 A4
由于c3列为定长类型,所欲NULL值也将占用记录的真实数据部分,所以使用0x00字节填充
而如果是变长数据类型,则不再记录的真实数据占用任何存储空间。
3.2.4 CHAR(M)列的存储格式
对于REDUNDANT行格式来说,不管使用的字符集是什么,只要使用CHAR(M)类型,该列的真实数据占用的内存空间大小就是该字符集表示一个字符最多需要的字节数和M的乘积。
比如utf8字符集的CHAR(10)类型的列,其真实数据占用的存储空间大小始终为30字节**。这样做,在更新数据的时候,就不需要申请新的存储空间了。**
3.3 溢出列
在COMPACT 行格式和REDUNDANT 行格式中,对于占用内存空间非常多的列,在记录真实数据处只会存储该列的一部分数据,将其他数据分散存储在几个其他页中。
并在记录真实数据处用20字节存储指向这些页的地址。
3.4 DYNAMIC行格式和COMPRESSED行格式
DYNAMIC行格式和COMPRESSED行格式在处理溢出列的时候,它们不会在记录的真实数据处存储该溢出列真实数据 的前 768 字节,而是把该列的所有真实数据都存储到溢出页中,只在记录的真实数据处存储 20 节大小的指向溢出页的地址
而COMPRESSED不同于DYNAMIC的一点在于会使用压缩算法进行压缩,以节省空间。
除了处理溢出的时候会于COMPACT 行格式不一样外,其他都于COMPACT 行格式很像。
参考: