如果你知道 MySQL 一行记录的存储结构,那么这个问题对你没什么难度。

如果你不知道也没关系,这次我跟大家聊聊 MySQL 一行记录是怎么存储的?

知道了这个之后,除了能应解锁前面这道面试题,你还会解锁这些面试题:

  • MySQL 的 NULL 值会占用空间吗?
  • MySQL 怎么知道 varchar(n) 实际占用数据的大小?
  • varchar(n) 中 n 最大取值为多少?
  • 行溢出后,MySQL 是怎么处理的?

这些问题看似毫不相干,其实都是在围绕「 MySQL 一行记录的存储结构」这一个知识点,所以攻破了这个知识点后,这些问题就引刃而解了。

好了,话不多说,发车!

MySQL 的数据存放在哪个文件?

大家都知道 MySQL 的数据都是保存在磁盘的,那具体是保存在哪个文件呢?

MySQL 存储的行为是由存储引擎实现的,MySQL 支持多种存储引擎,不同的存储引擎保存的文件自然也不同。

InnoDB 是我们常用的存储引擎,也是 MySQL 默认的存储引擎。所以,本文主要以 InnoDB 存储引擎展开讨论。

先来看看 MySQL 数据库的文件存放在哪个目录?

 

mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+-----------------------+
| Variable_name | Value                 |
+---------------+-----------------------+
| datadir       | /usr/local/var/mysql/ |
+---------------+-----------------------+
1 row in set (0.00 sec)

我们每创建一个 database(数据库) 都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。

比如,我这里有一个名为 my_test 的 database,该 database 里有一张名为 t_order 数据库表。

查看目录下面: /usr/local/var/mysql/ 可以看到有很多数据库文件:

我们每创建一个 database(数据库) 都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。

包含binlog和undolog文件;

mysql深入系列-一行记录是怎么存储的_字节数

 

比如,我这里有一个名为 my_test 的 database,该 database 里有一张名为 t_order 数据库表。

 

mysql深入系列-一行记录是怎么存储的_字节数_02

 

然后,我们进入 /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 文件。

好了,现在我们知道了一张数据库表的数据是保存在「 表名字.ibd 」的文件里的,这个文件也称为独占表空间文件。

表空间文件的结构是怎么样的?

表空间由段(segment)、区(extent)、页(page)、行(row)组成,InnoDB存储引擎的逻辑存储结构大致如下图:

 

mysql深入系列-一行记录是怎么存储的_数据_03

 

下面我们从下往上一个个看看。

#1、行(row)

数据库表中的记录都是按行(row)进行存放的,每行记录根据不同的行格式,有不同的存储结构。

后面我们详细介绍 InnoDB 存储引擎的行格式,也是本文重点介绍的内容。

#2、页(page)

记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。

因此,InnoDB 的数据是按「页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存

默认每个页的大小为 16KB,也就是最多能保证 16KB 的连续存储空间。

页的类型有很多,常见的有数据页、undo 日志页、溢出页等等

数据表中的行记录是用「数据页」来管理的,数据页的结构这里我就不讲细说了,之前文章有说过,感兴趣的可以去看这篇文章:换一个角度看 B+ 树

总之知道表中的记录存储在「数据页」里面就行。

3、区(extent)

 

表空间中的页实在是太多了,为了更好的管理这些页面,设计 InnoDB 的大叔们提出了 区 (英文名: extent ) 的概念。

对于16KB的页来说,连续的64个页就是一个区 ,也就是说一个区默认占用1MB空间大小。

不论是系统 表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组。画个图表示就是这样:

这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。

为啥好端端的提出一个 区 ( extent )的概念呢?  原因如下:

从理论上说,不引入 区 的概念只使用 页 的概念对存储引擎的运行并没啥影响,但是我 们来考虑一下下边这个场景:

我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的 B+ 树的节点中插入数 据。而 B+ 树的每一层中的页都会形成一个双向链表,如果是以 页 为单位来分配存储空间的话,双向链表相 邻的两个页之间的物理位置可能离得非常远。我们介绍 B+ 树索引的适用场景的时候特别提到范围查询只需 要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页 物理位置离得非常远,就是所谓的 随机I/O 。再一次强调,磁盘的速度和内存的速度差了好几个数量级, 随 机I/O 是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可 以使用所谓的 顺序I/O 。

所以,所以,所以才引入了 区 ( extent )的概念,一个区就是在物理位置上连续的64个页。在表中数据量大 的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照 区 为单位分配,甚至在表中的数据 十分非常特别多的时候,可以一次性分配多个连续的区。虽然可能造成一点点空间的浪费(数据不足填充满整个 区),但是从性能角度看,可以消除很多的随机 I/O ,功大于过嘛! 

存储结构如下:

 

mysql深入系列-一行记录是怎么存储的_字节数_04

其中 extent 0 ~ extent 255 这256个区算是第一个组,

extent 256 ~ extent 511 这256个区算是第二个 组,

extent 512 ~ extent 767 这256个区算是第三个组(上图中并未画全第三个组全部的区,请自行脑补),

依此类推可以划分更多的组。这些组的头几个页面的类型都是类似的,就像这样: 

 

每个区的内部存储结构如下:

mysql深入系列-一行记录是怎么存储的_字节数_05

 

4、段(segment)

  

表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段、索引段和回滚段等。

  • 索引段:存放 B + 树的非叶子节点的区的集合;
  • 数据段:存放 B + 树的叶子节点的区的集合;
  • 回滚段:存放的是回滚数据的区的集合,之前讲事务隔离 (opens new window)的时候就介绍到了 MVCC 利用了回滚段实现了多版本查询数据。

mysql深入系列-一行记录是怎么存储的_字段_06

 

InnoDB 行格式有哪些?

 

mysql深入系列-一行记录是怎么存储的_字节数_07

 

大家从图中可以看出来,一条完整的记录其实可以被分为 记录的额外信息 记录的真实数据 两大部分,下边我们详细看一下这两部分的组成。 

 

4.3.2.1 记录的额外信息 

这部分信息是服务器为了描述这条记录而不得不额外添加的一些信息,这些额外信息分为3类,分别是

变长字段长度列表 、 NULL值列表 和 记录头信息 ,我们分别看一下。 

 

1. 变长字段长度列表

varchar(n) 和 char(n) 的区别是什么,相信大家都非常清楚,char 是定长的,varchar 是变长的,变长字段实际存储的数据的长度(大小)不固定的。

,所以我 们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来,这样才不至于把 MySQL 服务器搞懵,所以 这些变长字段占用的存储空间分为两部分: 

1. 真正的数据内容

2. 占用的字节数 

在 Compact 行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长 字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放,我们再次强调一遍,是逆序存放! 

 

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 表里有这三条记录: 

mysql深入系列-一行记录是怎么存储的_字段_08

 

接下来,我们看看看看这三条记录的行格式中的 「变长字段长度列表」是怎样存储的。

先来看第一条记录:

  • name 列的值为 a,真实数据占用的字节数是 1 字节,十六进制 0x01;
  • phone 列的值为 123,真实数据占用的字节数是 3 字节,十六进制 0x03;
  • age 列和 id 列不是变长字段,所以这里不用管。

这些变长字段的真实数据占用的字节数会按照列的顺序逆序存放(等下会说为什么要这么设计),所以「变长字段长度列表」里的内容是「 03 01」,而不是 「01 03」。

这里是逆序存放的phone列在前,name列在后;

第一行数据:

 

mysql深入系列-一行记录是怎么存储的_数据_09

mysql深入系列-一行记录是怎么存储的_字节数_10

 

第二行数据:

同样的道理,我们也可以得出第二条记录的行格式中,「变长字段长度列表」里的内容是「 04 02」,如下图:

mysql深入系列-一行记录是怎么存储的_字段_11

 

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

mysql深入系列-一行记录是怎么存储的_字节数_12

 

为什么「变长字段长度列表」的信息要按照逆序存放?

 

这个设计是有想法的,主要是因为「记录头信息」中指向下一个记录的指针,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。

「变长字段长度列表」中的信息之所以要逆序存放,是因为这样可以使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。

同样的道理, NULL 值列表的信息也需要逆序存放。

 如果你不知道什么是 CPU Cache,可以看这篇文章 (opens new window),这属于计算机组成的知识。

每个数据库表的行格式都有「变长字段字节数列表」吗?

 

其实变长字段字节数列表不是必须的。

当数据表没有变长字段的时候,比如全部都是 int 类型的字段,这时候表里的行格式就不会有「变长字段长度列表」了,因为没必要,不如去掉以节省空间。

所以「变长字段长度列表」只出现在数据表有变长字段的时候。

2. NULL 值列表

表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。

如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。


  • 二进制位的值为1时,代表该列的值为NULL。
  • 二进制位的值为0时,代表该列的值不为NULL。

 

另外,NULL 值列表必须用整数个字节的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 0

 

mysql深入系列-一行记录是怎么存储的_数据_13

 

还是以 t_user 表的这三条记录作为例子:

mysql深入系列-一行记录是怎么存储的_字节数_14

 

 

接下来,我们看看看看这三条记录的行格式中的 NULL 值列表是怎样存储的。

先来看第一条记录,第一条记录所有列都有值,不存在 NULL 值,所以用二进制来表示是这样的:

 

mysql深入系列-一行记录是怎么存储的_字段_15

 

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

 

mysql深入系列-一行记录是怎么存储的_字节数_16

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

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

 

mysql深入系列-一行记录是怎么存储的_字节数_17

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

 

 

mysql深入系列-一行记录是怎么存储的_数据_18

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

mysql深入系列-一行记录是怎么存储的_字段_19

 

每个数据库表的行格式都有「NULL 值列表」吗?

 

NULL 值列表也不是必须的。

当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了。

所以在设计数据库表的时候,通常都是建议将字段设置为 NOT NULL,这样可以至少节省 1 字节的空间(NULL 值列表至少占用 1 字节空间)。

「NULL 值列表」是固定 1 字节空间吗?如果这样的话,一条记录有 9 个字段值都是 NULL,这时候怎么表示?

「NULL 值列表」的空间不是固定 1 字节的。

当一条记录有 9 个字段值都是 NULL,那么就会创建 2 字节空间的「NULL 值列表」,以此类推。

3. 记录头信息

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

 

mysql深入系列-一行记录是怎么存储的_字段_20

 

mysql深入系列-一行记录是怎么存储的_数据_21

 

  • delete_mask :标识此条数据是否被删除。从这里可以知道,我们执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_mask 标记为 1。
  • next_record:下一条记录的位置。从这里可以知道,记录与记录之间是通过链表组织的。在前面我也提到了,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。
  • record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录

记录的真实数据

记录的真实数据 除了 c1 、 c2 、 c3 、 c4 这几个我们自己定义的列的数据 以外, MySQL 会为每个记录默认的添加一些列(也称为 隐藏列 ),具体的列如下: 

 

mysql深入系列-一行记录是怎么存储的_数据_22

 

mysql深入系列-一行记录是怎么存储的_字段_23

 

如果你熟悉 MVCC 机制,你应该就清楚 trx_id 和 roll_pointer 的作用了,如果你还不知道 MVCC 机制,可以看完这篇文章 (opens new window),一定要掌握,面试也很经常问 MVCC 是怎么实现的。

varchar(n) 中 n 最大取值为多少?

我们要清楚一点,MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节。

也就是说,一行记录除了 TEXT、BLOBs 类型的列,限制最大为 65535 字节,注意是一行的总长度,不是一列。

知道了这个前提之后,我们再来看看这个问题:「varchar(n) 中 n 最大取值为多少?」

varchar(n) 字段类型的 n 代表的是最多存储的字符数量,并不是字节大小哦。

要算 varchar(n) 最大能允许存储的字节数,还要看数据库表的字符集,因为字符集代表着,1个字符要占用多少字节,

比如 ascii 字符集, 1 个字符占用 1 字节,那么 varchar(100) 意味着最大能允许存储 100 字节的数据。

 

一个表一个字段的情况:

前面我们知道了,一行记录最大只能存储 65535 字节的数据。

那假设数据库表只有一个 varchar(n) 类型的列且字符集是 ascii,在这种情况下, varchar(n) 中 n 最大取值是 65535 吗?

我们定义一个 varchar(65535) 类型的字段,字符集为 ascii 的数据库表。

看能不能成功创建一张表:

CREATE TABLE greatName ( 
`name` VARCHAR(65535)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;
mysql> CREATE TABLE greatName (
    -> `name` VARCHAR(65535)  NULL
    -> ) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. 
This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs

可以看到,创建失败了。

从报错信息就可以知道一行数据的最大字节数是 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) 是否可行?

CREATE TABLE greatName ( 
`name` VARCHAR(65533)  NULL
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;

 

mysql深入系列-一行记录是怎么存储的_字段_24

 

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

mysql深入系列-一行记录是怎么存储的_字节数_25

 

可以看到,创建成功了。说明我们的推论是正确的,在算 varchar(n) 中 n 最大值时,需要减去 「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。

当然,我上面这个例子是针对字符集为 ascii 情况,如果采用的是 UTF-8,varchar(n) 最多能存储的数据计算方式就不一样了:

  • 在 UTF-8 字符集下,一个字符最多需要三个字节,varchar(n) 的 n 最大取值就是 65532/3 = 21844。

上面所说的只是针对于一个字段的计算方式。

#多字段的情况

如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。

mysql深入系列-一行记录是怎么存储的_字段_26

 

行溢出后,MySQL 是怎么处理的?

 MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB,也就是 16384字节,而一个 varchar(n) 类型的列最多可以存储 65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的「溢出页」中。

 

最后需要注意的是,不只是 VARCHAR(M) 类型的列,其他的 TEXT、BLOB 类型的列在存储数据非常多的时候 也会发生 行溢出 。

如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。在一般情况下,InnoDB 的数据都是存放在 「数据页」中。但是当发生行溢出时,溢出的数据会存放到「溢出页」中。

当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。大致如下图所示。

 

 

mysql深入系列-一行记录是怎么存储的_字段_27

上面这个是 Compact 行格式在发生行溢出后的处理。

Compressed 和 Dynamic 这两个行格式和 Compact 非常类似,主要的区别在于处理行溢出数据时有些区别。

这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中,看起来就像下面这样:

mysql深入系列-一行记录是怎么存储的_字节数_28

Compressed 行格式和 Dynamic 不同的一点是, Compressed 行格式会采用压缩算法对页面进行压缩,以节省空 间。 

 

行溢出的临界点

那发生 行溢出 的临界点是什么呢?也就是说在列存储多少字节的数据时就会发生 行溢出 ? 

MySQL 中规定一个页中至少存放两行记录,至于为什么这么规定我们之后再说,现在看一下这个规定造成的影 响。以上边的 varchar_size_demo 表为例,它只有一个列 c ,我们往这个表中插入两条记录,每条记录最少插入 多少字节的数据才会 行溢出 的现象呢?这得分析一下页中的空间都是如何利用的。 

每个页除了存放我们的记录以外,也需要存储一些额外的信息,乱七八糟的额外信息加起来需要 136 个字节 的空间(现在只要知道这个数字就好了),其他的空间都可以被用来存储记录。

每个记录需要的额外信息是 27 字节。

这27个字节包括下边这些部分:

  • 2个字节用于存储真实数据的长度
  • 1个字节用于存储列是否是NULL值
  • 5个字节大小的头信息
  • 6个字节的 row_id 列
  • 6个字节的 transaction_id 列
  • 7个字节的 roll_pointer 列 

假设一个列中存储的数据字节数为n,那么发生 行溢出 现象时需要满足这个式子: 

136 + 2×(27 + n) > 16384

求解这个式子得出的解是: n > 8098 。也就是说如果一个列中存储的数据不大于 8098 个字节,那就不会发生 行溢出 ,否则就会发生 行溢出 。不过这个 8098 个字节的结论只是针对只有一个列的 varchar_size_demo 表来

说的,如果表中有多个列,那上边的式子和结论都需要改一改了,所以重点就是:你不用关注这个临界点是什 么,只要知道如果我们想一个行中存储了很大的数据时,可能发生 行溢出 的现象。 

 

 

总结

MySQL 的 NULL 值是怎么存放的?

MySQL 的 Compact 行格式中会用「NULL值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。

NULL值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。

 

MySQL 怎么知道 varchar(n) 实际占用数据的大小?

MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。

 

varchar(n) 中 n 最大取值为多少?

一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL值列表所占用的字节数」。所以, 我们在算 varchar(n) 中 n 最大值时,需要减去这两个列表所占用的字节数。

如果一张表只有一个 varchar(n) 字段,且允许为 NULL,字符集为 ascii。varchar(n) 中 n 最大取值为 65532。

计算公式:65535 - 变长字段字节数列表所占用的字节数 - NULL值列表所占用的字节数 = 65535 - 2 - 1 = 65532。

如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。

行溢出后,MySQL 是怎么处理的?

 

如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。

Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。

 Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。

 

 

 

 

参考资料:

  • 《MySQL 是怎样运行的》
  • 《MySQL技术内幕 InnoDB存储引擎》

 

 

 

 

 

 

 

 

Face your past without regret. Handle your present with confidence.Prepare for future without fear. keep the faith and drop the fear. 面对过去无怨无悔,把握现在充满信心,备战未来无所畏惧。保持信念,克服恐惧!一点一滴的积累,一点一滴的沉淀,学技术需要不断的积淀!