我们平时对 MySQL 的了解都只是限制在使用层面上,但是难道你就没有一个时刻好奇 MySQL 的内部结构嘛,我们通过 SQL 语句插入的一条条记录在 MySQL 底层到底是以什么格式存储的呢 ? 本文就将以 InnoDB 存储引擎为例子,介绍 MySQL 存储引擎的数据存储结构
背景知识
InnoDB 存储引擎将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB 。 ( 不然一条条从磁盘中读数据很慢 )
一、什么是行格式
我们插入的一条记录在磁盘中的存放方式称为行格式(也叫记录格式),InnoDB 共具有 4 种不同类似的行格式 。
指定某个表的行格式方法
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
二、Compact 行格式
1. 记录的额外信息
—— 为了描述该条记录不得不添加的信息
1.1 变长字段长度列表
变长字段包括: VARCHAR(MAX) 、 VARBINARY(MAX) 、各种 TEXT 类型,各种 BLOB 类型 等, 其占用的存储空间分为 真正的数据内容 和 占用的字节数 两部分。因为变长字段中存储多少字节的数据是不固定的 , 所以我们需要存储变长字段长度,从而提示 MySQL 服务器。
背景设定:
字符 —— 相对于字符集提出 (ASSCI, UTF8 等)。
M —— 该类型最多存放 M 个字符 , 例如 varchar(M)。
W —— 一个字符最多需要的字节数,如 ASSIC 字符集1个字符最多需要1个字节。
L —— 当前存放的值实际占用的字节数
- W * M <= 255 ,用 1 个字节来表示真正字符串占用的字节数 ( 一个字节 8 位 , 表示的最大整数是 255)
- M×W > 255
(1)L <= 127 ,则用1个字节来表示真正字符串占用的字节数
(2)L > 127 ,则用2个字节来表示真正字符串占用的字节数
为什么是 127 ? 节约空间,减少空间的浪费总结:如果该可变字段允许存储的最大字节数超过255字节并且真实存储的字节数超过127字节,则使用2个字节,否则使用1个字节
注意: 1. 存储的长度值按照列逆序排放 2. 不存储 null 值的长度
1.2 NULL 值列表
NULL 值都记录到真实数据中很占空间,所以统一使用空值列表管理空值
处理过程:
- 统计出允许存储空值的列 (没有允许存储 NULL 的列,NULL值列表不存在)
- 允许存储空值的列对应一个二进制位,二进制位为 1 , 表示该列为 NULL
- 空值列表必须用整数个字节表示,不是整数个字节的话,则在最高位补 0
1.3 记录头信息
记录头信息由固定的 5 个字节组成:
各个区域表示的含义如下 :
2. 记录的真实数据
3 个隐藏列 (行ID + 事务 id + 回滚指针) + 我们自己定义的列
其中 行ID 是可选的,这是由主键生成策略决定的 。
主键生成策略:
- 优先使用用户自定义主键作为主键
- 如果用户没有定义主键,则选取一个 Unique 键作为主键
- Unique 键没有定义,默认添加一个名为 row_id 的隐藏列作为主键。
3. 有关 CHAR(M) 的特殊性
- 定长字符集(ASSIC、UTF8等) : 该列占用的字节数不会被加到变长字段长度列表
- 变长字符集(如 gck)时:该列占用的字节数会被加到变长字段长度列表。
变长字符集下的 CHAR(M) 类型的列要求至少占用 M 个字节,即使向该列中存储一个空字符串也会占用 M 个字节。
目的: 更新数据时,不长于 M 字节的数据可以在该记录处直接更新,而不是在存储空间中重新分配一个新的记录空间,导致原有的记录空间成为碎片。
三、 Redundant 行格式 (过时)/rɪˈdʌn.dənt/
其结构如下 ——
1. 字段长度偏移列表
实质上是存储每个列中的值占用的空间在 记录的真实数据 处结束的位置,其是一堆差值的组合。
2. 有关行溢出
2.1 VARCHAR(M) 类型的列 —— 有关 M 的行溢出
65535 —— 一列的最多字节数 (2 的 16 次方 - 1)
- ASSIC 类型 —— 即一个字符占用一个字节
- NULL - 存储 65532 个字节的数据,包含1个字节长度的空表和2个字节的长度表
- NOT NULL - 存储 65533 个字节的数据,因为其不含空表
如果使用的不是 ascii 字符集,那 M 的最大取值取决于该字符集表示一个字符最多需要的字节数。
在列的值允许为 NULL 的情况下:
- gbk - M 的最大取值就是 32766 (65532/2),即最多能存储 32766 个字符;
- utf8 - M 的最大取值就是 21844 (65532/3)个字符
2.2 因为数据太长导致的溢出 —— 源于一个页放不了一条记录的情况
当一个行中存储了很大的数据时,可能发生 行溢出 的现象
Compact 和 Reduntant 行格式中,对于占用存储空间非常大的列,在 记录的真实数据 处只会存储该列的一部分数据,把剩余的数据分散存储在溢出页中,记录的真实数据处用20个字节存储指向这些页的地址,从而可以找到剩余数据所在的页
2.3 行溢出临界值
MySQL 中规定一个页中至少存放两行记录 , 页中存放的乱七八糟的数据也需要 136 个字节 。 每个记录需要 27 个字节的额外数据。
136 + 2 × (27 + n) > 1024 * 16 (16 KB)
计算得: 在只有一个列的情况下,如果一个列中存储的数据不大于 8098 个字节,那就不会发生行溢出 ,否则就会发生行溢出 。
四、Dynamic 行格式(5.7 默认的)& Compressed 行格式
存储结构类似 Compact 行格式,只在处理 行溢出 时不同,其不记录真实数据的前 768 个字节,而是所有字节都存储到溢出页面中,只记录真实数据所在的地址。
Compressed vs Dynamic —— Compressed 行格式会采用压缩算法对页面进行压缩,以节省空间。