InnoDB存储引擎详解
InnoDB 存储引擎
InnoDB 存储引擎是用 B+ 树来组织数据的
一行记录是怎么存储的
MySQL 的数据存放在哪个文件
- 以下均以默认存储引擎 InnoDB 为例。
- 不同存储引擎保存的文件不一样
数据库文件存放目录
mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+-----------------+
| Variable\_name | Value |
+---------------+-----------------+
| datadir | /var/lib/mysql/ |
+---------------+-----------------+
1 row in set (0.00 sec)我们每创建一个 database(数据库) 都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。
比如,我这里有一个名为 my_test 的 database,该 database 里有一张名为 t_order 数据库表。

然后,我们进入 /var/lib/mysql/my_test 目录,看看里面有什么文件?
[root@xiaolin ~]#ls /var/lib/mysql/my\_test
db.opt
t\_order.frm
t\_order.ibd可以看到,共有三个文件,这三个文件分别代表着:
- db.opt:用来存储当前数据库的默认字符集和字符校验规则。
- t_order.frm :t_order 的表结构会保存在这个文件。
- 在 MySQL 中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。
- t_order.ibd:t_order 的表数据会保存在这个文件。
- 表数据既可以存在共享表空间文件(文件名:ibdata1) 也可以存放在独占表空间文件(文件名:表名字.ibd)
- 这个行为是由参数 innodb_file_per_table控制的 若设置了参数 innodb_file_per_table 为 1,则会将存储的数据、索引等信息单独存储在一个独占表空间,从 MySQL 5.6.6 版本开始,它的默认值就是 1 了, 因此从这个版本之后, MySQL 中每一张表的数据都存放在一个独立的 .ibd 文件。
不同的存储引擎,数据库的文件结构不一样,和索引是否支持聚簇索引密切相关
- myISAM:因为表数据和索引数据分别放在两个文件,是不支持聚簇索引的
- InnoDB:因为 .ibd 中存储了表的行数据和索引数据,是支持聚簇索引的
表空间文件的结构
表空间:由段(segment)、区(extent)、页(page)、行(row)组成
InnoDB存储引擎的逻辑存储结构大致如下图:

从下往上看
行(row)
数据库表中的记录都是按行(row)进行存放的,每行记录根据不同的行格式,有不同的存储结构。
页(page)
- **读取并不以「行」为单位:**记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。
- **按「页」为单位来读写的:**因此,InnoDB 的数据是按「页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。
- **默认每个页的大小为 16KB:**也就是最多能保证 16KB 的连续存储空间。
- **页是 InnoDB 存储引擎磁盘管理的最小单元:**意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。
- **页的类型:**有很多,常见的有数据页、undo 日志页、溢出页等等。数据表中的行记录是用「数据页」来管理的,数据页的结构:换一个角度看 B+ 树(opens new window)
总之知道表中的记录存储在「数据页」里面就行。
区(extent)
InnoDB 存储引擎是用 B+ 树来组织数据的。
B+ 树中每一层都是通过双向链表连接起来的,如果是以页为单位来分配存储空间,那么链表中相邻的两个页之间的物理位置并不是连续的,可能离得非常远,那么磁盘查询时就会有大量的随机I/O,随机 I/O 是非常慢的。
解决这个问题也很简单,就是让链表中相邻的页的物理位置也相邻,这样就可以使用顺序 I/O 了,那么在范围查询(扫描叶子节点)的时候性能就会很高。
那具体怎么解决呢?
在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。
每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。
段(segment)
表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。
段一般分为数据段、索引段和回滚段等。
- 索引段:存放 B + 树的非叶子节点的区的集合;
- 数据段:存放 B + 树的叶子节点的区的集合;
- 回滚段:存放的是回滚数据的区的集合,MVCC 利用了回滚段实现了多版本查询数据。
InnoDB 行格式有哪些?
行格式(row_format),就是一条记录的存储结构。
InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed 行格式。
- Redundant 是很古老的行格式了, MySQL 5.0 版本之前用的行格式,现在基本没人用了。
- 由于 Redundant 不是一种紧凑的行格式,所以 MySQL 5.0 之后引入了 Compact 行记录存储方式,Compact 是一种紧凑的行格式,设计的初衷就是为了让一个数据页中可以存放更多的行记录,从 MySQL 5.1 版本之后,行格式默认设置成 Compact。
- Dynamic 和 Compressed 两个都是紧凑的行格式,它们的行格式都和 Compact 差不多,因为都是基于 Compact 改进一点东西。
从 MySQL5.7 版本之后,默认使用 Dynamic 行格式。
重点理解 Compact 行格式,因为 Dynamic 和 Compressed 这两个行格式跟 Compact 非常像。
compact 行格式
「记录的额外信息」+「记录的真实数据」

- 变长字段长度列表
varchar(n) 和 char(n) 的区别是:char 是定长的,varchar 是变长的,变长字段实际存储的数据的长度(大小)不固定的。
所以,在存储数据的时候,也要把数据占用的大小存起 来,存到「变长字段长度列表」里面,读取数据的时候才能根据这个「变长字段长度列表」去读取对应长度的数据。其他 TEXT、BLOB 等变长字段也是这么实现的。
「变长字段长度列表」具体是怎么保存「变长字段的真实数据占用的字节数」:
我们先创建这样一张表,字符集是 ascii(所以每一个字符占用的 1 字节),行格式是 Compact,t_user 表中 name 和 phone 字段都是变长字段:
CREATE TABLE
t\_user
(
id
int(11) NOT NULL,
name
VARCHAR(20) DEFAULT NULL,
phone
VARCHAR(20) DEFAULT NULL,
age
int(11) DEFAULT NULL,
PRIMARY KEY (
id
) USING BTREE
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW\_FORMAT = COMPACT;现在 t_user 表里有这三条记录:

第一条记录:
- name 列的值为 a,真实数据占用的字节数是 1 字节,十六进制 0x01;
- phone 列的值为 123,真实数据占用的字节数是 3 字节,十六进制 0x03;
- age 列和 id 列不是变长字段,所以这里不用管。
这些变长字段的真实数据占用的字节数会按照列的顺序逆序存放,所以「变长字段长度列表」里的内容是「 03 01」,而不是 「01 03」。

第二条记录:
「变长字段长度列表」里的内容是「 04 02」,如下图:

第三条记录:
phone 列的值是 NULL,NULL 是不会存放在行格式中记录的真实数据部分里的,所以「变长字段长度列表」里不需要保存值为 NULL 的变长字段的长度。

为什么 「变长字段长度列表」 要按 逆序存放?
主要是因为「记录头信息」中指向下一个记录的指针,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是:向左读就是记录头信息,向右读就是真实数据,比较方便。
「变长字段长度列表」中的信息之所以要逆序存放,是因为这样可以使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。
同样的道理, NULL 值列表的信息也需要逆序存放
每个数据库表的行格式都有「变长字段字节数列表」吗?
其实变长字段字节数列表不是必须的。
当数据表没有变长字段的时候,比如全部都是 int 类型的字段,这时候表里的行格式就不会有「变长字段长度列表」了,因为没必要,不如去掉以节省空间。
所以「变长字段长度列表」只出现在数据表有变长字段的时候
- NULL 值列表
表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。
如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。
- 二进制位的值为1时,代表该列的值为NULL。
- 二进制位的值为0时,代表该列的值不为NULL。
另外,NULL 值列表必须用整数个字节的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 0。
还是以 t_user 表的这三条记录作为例子:

接下来,我们看看看看这三条记录的行格式中的 NULL 值列表是怎样存储的。
第一条记录,第一条记录所有列都有值,不存在 NULL 值,所以用二进制来表示是酱紫的:

但是 InnoDB 是用整数字节的二进制位来表示 NULL 值列表的,现在不足 8 位,所以要在高位补 0,最终用二进制来表示是的:

所以,对于第一条数据,NULL 值列表用十六进制表示是 0x00。
第二条记录,第二条记录 age 列是 NULL 值,所以,对于第二条数据,NULL值列表用十六进制表示是 0x04。

最后第三条记录,第三条记录 phone 列 和 age 列是 NULL 值,所以,对于第三条数据,NULL 值列表用十六进制表示是 0x06。

我们把三条记录的 NULL 值列表都填充完毕后,它们的行格式是这样的:

每个数据库表的行格式都有「NULL 值列表」吗?
NULL 值列表也不是必须的。
当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了。
所以在设计数据库表的时候,通常都是建议将字段设置为 NOT NULL,这样可以至少节省 1 字节的空间(NULL 值列表至少占用 1 字节空间)。
「NULL 值列表」是固定 1 字节空间吗?
如果这样的话,一条记录有 9 个字段值都是 NULL,这时候怎么表示?
「NULL 值列表」的空间不是固定 1 字节的。
当一条记录有 9 个字段值都是 NULL,那么就会创建 2 字节空间的「NULL 值列表」,以此类推。
记录头信息
记录头信息中包含的内容很多,个比较重要的:
- delete_mask :标识此条数据是否被删除。 从这里可以知道,我们执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_mask 标记为 1。
- next_record:下一条记录的位置。 从这里可以知道,记录与记录之间是通过链表组织的。在前面我也提到了,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。
- record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录
- 记录的真实数据
记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:
- row_id
- trx_id
- roll_pointer

- row_id:如果我们建表的时候指定了主键或者唯一约束列,那么就没有 row_id 隐藏字段了。 如果既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段。 row_id不是必需的,占用 6 个字节。
- trx_id:事务id,表示这个数据是由哪个事务生成的。 trx_id是必需的,占用 6 个字节。
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录 roll_pointer 是必需的,占用 7 个字节。
varchar(n)中 n 最大取值多少
一行记录除了 TEXT、BLOBs 类型的列,限制最大为 65535 字节,注意是一行的总长度,不是一列。
n 代表什么
varchar(n) 字段类型的 n 代表的是最多存储的字符数量,并不是字节大小
计算最大 n
要算 varchar(n) 最大能允许存储的字节数,还要看数据库表的字符集
- n = 当前行可用最大字节 / 当前字符集一个字符的字节大小
- 当前行可用最大字节 = 65535 - 非真实数据部分所占字节 - 隐藏字段所占字节
单字段的情况下
我们定义一个 varchar(65535) 类型的字段,字符集为 ascii 的数据库表。
CREATE TABLE test (
name
VARCHAR(65535) NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW\_FORMAT = COMPACT;看能不能成功创建一张表:

可以看到,创建失败了。
从报错信息就可以知道一行数据的最大字节数是 65535(不包含 TEXT、BLOBs 这种大对象类型),其中包含了 storage overhead。
问题来了,这个 storage overhead 是什么呢?其实就是「变长字段长度列表」和 「NULL 值列表」,也就是说一行数据的最大字节数 65535,其实是包含「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。
所以, 我们在算 varchar(n) 中 n 最大值时,需要减去 storage overhead 占用的字节数。
这是因为我们存储字段类型为 varchar(n) 的数据时,其实分成了三个部分来存储:
- 真实数据
- 真实数据占用的字节数
- NULL 标识,如果不允许为NULL,这部分不需要
本次案例中,「NULL 值列表」所占用的字节数是多少?
前面我创建表的时候,字段是允许为 NULL 的,所以会用 1 字节来表示「NULL 值列表」。
本次案例中,「变长字段长度列表」所占用的字节数是多少?
「变长字段长度列表」所占用的字节数 = 所有「变长字段长度」占用的字节数之和。
所以,我们要先知道每个变长字段的「变长字段长度」需要用多少字节表示?具体情况分为:
- 条件一:如果变长字段允许存储的最大字节数小于等于 255 字节,就会用 1 字节表示「变长字段长度」;
- 条件二:如果变长字段允许存储的最大字节数大于 255 字节,就会用 2 字节表示「变长字段长度」;
我们这里字段类型是 varchar(65535) ,字符集是 ascii,所以代表着变长字段允许存储的最大字节数是 65535,符合条件二,所以会用 2 字节来表示「变长字段长度」。
因为我们这个案例是只有 1 个变长字段,所以「变长字段长度列表」= 1 个
变长字段长度」占用的字节数,也就是 2 字节。
因为我们在算 varchar(n) 中 n 最大值时,需要减去 「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。
在数据库表只有一个 varchar(n) 字段且字符集是 ascii 的情况下:varchar(n) 中 n 最大值 = 65535 - 2 - 1 = 65532。
我们先来测试看看 varchar(65533) 是否可行?

可以看到,还是不行,接下来看看 varchar(65532) 是否可行?

可以看到,创建成功了。
说明我们的推论是正确的,在算 varchar(n) 中 n 最大值时,需要减去 「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。
当然,我上面这个例子是针对字符集为 ascii 情况,如果采用的是 UTF-8,varchar(n) 最多能存储的数据计算方式就不一样了:
- 在 UTF-8 字符集下,一个字符最多需要三个字节,varchar(n) 的 n 最大取值就是 65532/3 = 21844。
多字段的情况下
如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。

行溢出后,MySQL 怎么处理
Compact 行格式在发生行溢出后的处理
MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB,也就是 16384字节,而一个 varchar(n) 类型的列最多可以存储 65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的「溢出页」中。
如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。
在一般情况下,InnoDB 的数据都是存放在 「数据页」中。
但是当发生行溢出时,溢出的数据会存放到「溢出页」中。
**当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,**然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。大致如下图所示。

Compressed 和 Dynamic
Compressed 和 Dynamic 这两个行格式和 Compact 非常类似,主要的区别在于处理行溢出数据时有些区别。
这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。
而实际的数据都存储在溢出页中,看起来就像下面这样:

char 与 varchar 的区别
- 主要从存储机制、性能差异理解
测试环境
- MySQL:5.7.36 版本
- 存储引擎: Innodb
- 行格式(Row format):DYNAMIC
Innodb存储引擎支持多种行格式:REDUNDANT、COMPACT、DYNAMIC、COMPRESSED,不同行格式存储方式存在差异。
默认行格式可以通过innodb_default_row_format变量查看:
mysql> show variables like "innodb\_default\_row\_format";
+---------------------------+---------+
| Variable\_name | Value |
+---------------------------+---------+
| innodb\_default\_row\_format | dynamic |
+---------------------------+---------+
1 row in set (0.08 sec)varchar和char在MySQL层的区别
根据MySQL的官方文档The CHAR and VARCHAR Types中的描述, varchar和char的区别主要有:
- 最大长度:char是255,varchar是65535,单位是字符(而不是字节)。
- 尾随空格:char会将尾随空格去掉,而varchar不会。 因为存储时,char会用空格填充至指定长度,所以取出时需要去除空格。 如果char字段有唯一索引,a和a 会提示唯一索引冲突。
- 存储空间占用:varchar会占用额外的1~2字节来存储字符串长度。如果最大长度超过255,就需要2字节,否则1字节。
注意:这是MySQL层的描述,具体怎么存储由存储引擎决定。
varchar和char在存储引擎层的区别
以下描述基于Innodb存储引擎、DYNAMIC行格式。
varchar如何存储
下面通过一个实验来看看,varchar在Innodb底层是如何存储的。
- 创建一个带有varchar字段的表格,并插入2条记录:
mysql> drop table test\_string;
Query OK, 0 rows affected (0.02 sec)
mysql> create table test\_string(
-> id int(11) unsigned not null primary key auto\_increment,
-> code varchar(100) not null
-> )charset=utf8mb4;;
Query OK, 0 rows affected (0.03 sec)
mysql> insert into test\_string values(1, "hello");
Query OK, 1 row affected (0.01 sec)
mysql> insert into test\_string values(2, "hhhhhhhhhh");
Query OK, 1 row affected (0.01 sec)找到数据文件/var/lib/mysql/study/test_string.ibd,使用hexdump命令查看:
hexdump -C test.ibd
...... // 省略一堆
0000c060 02 00 1b 69 6e 66 69 6d 75 6d 00 03 00 0b 00 00 |...infimum......|
0000c070 73 75 70 72 65 6d 75 6d 05 00 00 10 00 1c 00 00 |supremum........|
0000c080 00 01 00 00 00 00 08 28 c3 00 00 01 8d 01 10 68 |.......(.......h|
0000c090 65 6c 6c 6f 0a 00 00 18 ff d6 00 00 00 02 00 00 |ello............|
0000c0a0 00 00 08 2a c5 00 00 01 d1 01 10 68 68 68 68 68 |...\*.......hhhhh|
0000c0b0 68 68 68 68 68 00 00 00 00 00 00 00 00 00 00 00 |hhhhh...........|
0000c0c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
...... // 省略一堆其中,65 6c 6c 6f、 68 68 68 68 68 68 68 68 68 68为实际存储的数据:hello和hhhhhhhhhh
05和0a分别对应字符串的长度5和10
插入2条中文(多字节编码),分别是6个字和100个字:
mysql> insert into test\_string values(3, "毛毛毛毛毛毛");
Query OK, 1sq row affected (0.01 sec)
mysql> insert into test\_string
values(4, "毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛
毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛");
Query OK, 1 row affected (0.01 sec)再次用hexdump命令查看:
...... // 省略一堆
0000c0b0 68 68 68 68 68 12 00 00 20 00 2a 00 00 00 03 00 |hhhhh... .\*.....|
0000c0c0 00 00 00 08 3f d3 00 00 01 d9 01 10 e6 af 9b e6 |...\*.......aaaaaa|
0000c0d0 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b 2c 81 |..............,.|
0000c0e0 00 00 28 ff 8b 00 00 00 04 00 00 00 00 08 41 d5 |..(...........A.|
0000c0f0 00 00 01 9f 01 10 e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c100 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af |................|
0000c110 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b |................|
0000c120 e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c130 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af |................|
0000c140 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b |................|
0000c150 e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c160 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af |................|
0000c170 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b |................|
0000c180 e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c190 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af |................|
0000c1a0 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b |................|
0000c1b0 e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c1c0 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af |................|
0000c1d0 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b |................|
0000c1e0 e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c1f0 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af |................|
0000c200 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b |................|
0000c210 e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c220 af 9b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
...... // 省略一堆从输出中可以看到大量重复的e6 af 9b,这是使用utf8mb4编码的"毛"字。
可以使用hex()函数来验证:
mysql> select hex('毛');
+------------+
| hex('毛') |s
+------------+
| E6AF9B |
+------------+
1 row in set (0.09 sec)1个“毛”字占用3个字节,插入的第1条中文“毛毛毛毛毛毛”占用18个字节, 对应上面标红的12(十六进制,转换为十进制是18)。
类似的,第2条中文记录,100个汉字占用300个字节,对应上面的2c 81
2c 81是逆序存储(行格式决定),转过来之后是81 2c。
其中第1位二进制位表示字符长度大于127字节,除去第1位后,即01 2c对应十进制的300
从上面的实验可以看出,varchar类型对于短字符串、长字符串、多字节编码,都是存储了实际的字符+字符长度。
char如何存储
接下来对比char类型的字符串,看看有什么区别。
- 先创建一个表格:
mysql> create table test\_char(
-> id int unsigned not null primary key auto\_increment,
-> code char(50)
-> ) charset=utf8mb4;
Query OK, 0 rows affected (0.03 sec)注意表格字段定义是char(50),编码是 utf8mb4
意味code字段着最多可以存储50*4=200个字节的数据。
- 接下来插入几条测试数据(5个字母、50个字母、50个多字节文字):
mysql> insert into test\_char values(1, "hello"),
(2, 'aaaaasaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'),
(3, "毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛毛");
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0再次使用hexdump命令查看文件中的数据:
0000c060 02 00 1c 69 6e 66 69 6d 75 6d 00 04 00 0b 00 00 |...infimum......|
0000c070 73 75 70 72 65 6d 75 6d 32 00 00 00 10 00 4a 00 |supremum2.....J.|
0000c080 00 00 01 00 00 00 00 08 4a dc 00 00 01 dd 01 10 |........J.......|
0000c090 68 65 6c 6c 6f 20 20 20 20 20 20 20 20 20 20 20 |hello |
0000c0a0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |
0000c0b0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |
0000c0c0 20 20 32 00 00 00 18 00 4a 00 00 00 02 00 00 00 | 2.....J.......|
0000c0d0 00 08 4a dc 00 00 01 dd 01 1c 61 61 61 61 61 61 |..J.......aaaaaa|
0000c0e0 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
0000c0f0 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
0000c100 61 61 61 61 61 61 61 61 61 61 61 61 96 00 00 00 |aaaaaaaaaaaa....|
0000c110 20 ff 5d 00 00 00 03 00 00 00 00 08 4a dc 00 00 | .].........J...|
0000c120 01 dd 01 28 e6 af 9b e6 af 9b e6 af 9b e6 af 9b |...(............|
0000c130 e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c140 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af |................|
0000c150 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b |................|
0000c160 e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c170 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af |................|
0000c180 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b |................|
0000c190 e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 |................|
0000c1a0 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af 9b e6 af |................|
0000c1b0 9b e6 af 9b e6 af 9b e6 af 9b 00 00 00 00 00 00 |................|
0000c1c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
0000c1d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|从上面内容可以看出,对于char类型的字段,innodb同样保存了字符长度。
对于utf8mb4 char(50)来说,长度不够50字节的字符串,会使用空格(0x20)填充到50个字节(但不是最大长度200)。
char和varchar存储对比
- char和varchar都会存储字符串长度
- 对于CHAR(N)字段,如果实际存储数据小于N字节,会填充空格到N个字节。
性能对比
从char和varchar的存储结构对比,可以得出一个结论:
char填充空格可能导致浪费存储空间,进而导致性能下降。
因为char多存储一些空格,意味着需要从磁盘读写更多的数据、耗费更多内存、查找数据时删除空格可能也会耗费一些CPU性能。
那与varchar相比,char字段是不是一无是处呢?
大部分情况,是的,最好使用varchar。
不过考虑一个极端的场景:某个字段的最大长度是100字节,但是会频繁修改。如果使用char(100),则插入记录后就分配了100个字节,后续修改不会造成页分裂、页空隙等问题,而varchar(100)由于没有提前分配存储空间,后续修改时可能出现页分裂,进而导致性能下降。
数据页
。。。。。