一文讲清,MySQL数据库一行数据在磁盘上是怎么存储的?

162 阅读7分钟

数据库给使用者最直观的感觉,就是库、表、行、字段,这些概念都是逻辑上的。前面我们深入讲解了Buffer Pool的内部原理,它的基本存储单位是默认大小为16K的页。每页都保存了一行一行的数据。我们按照数据页为单位把磁盘上的数据加载到内存的缓存页里来,也是按照页为单位,把缓存页的数据刷入磁盘的数据页中。

而我们常常听到数据页、数据区、表空间这些名词,其实这些名词是物理层面上的概念。我们不经要问,库、表、行、字段,这些逻辑上的概念是如何对应到物理层的概念上的呢?我们查询一行数据,是如何找到条数据所在的数据页的呢?

接下来,笔者用几篇文章,讲解下MySQL的表空间、数据区、数据页这些概念,当大家明白搞明白这些东西后,就自然理解上面的问题了。

行格式

行格式即行记录的物理存储格式,决定了这张表数据的物理存储方式,会影响crud性能。

指定行格式

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称;
ALTER TABLE 表名 ROW_FORMAT=行格式名称;

InnoDB 包含以下四种行格式

  • Compact

  • Redundant

  • Dynamic

  • Compressed

mysql5.7之前的版本使用的是compact行格式,5.7及以后的版本使用的是dynamic行格式。Compact和Dynamic应用较广泛,compact是目前使用最多的一种,而dynamic是新版本默认的行记录格式。

初识MySQL行数据存储格式

我们这里就以Compact存储格式来讲解。

Compact行格式下,每行数据的存储格式,大概是这样的:

变长字段(记录的长度)列表,null值列表,数据头,col1的值,col2的值,col3的值......

Compact 中一条完整的记录可以被分成'记录的额外信息'和'记录的真实数据' 两部分,其他三种存储存储格式基本大同小异。

变长字段是如何存储的?

MySQL中变长字段长度是不固定的, 比如VARCHAR(50),实际存储的内容可能是"hello",也可能是“hello world"。

所以,我们要读取这类数据字段,必须要知道它的长度。

比如一行数据,它的几个字段为VARCHAR(10), VARCHAR(5)

,VARCHAR(20),CHAR(1),CHAR(1),插入一行数据:hello, ni, hao, a, b。

此时磁盘中存储的行开头的变长字段长度列表,必须存储几个变长字段的长度,需要注意的是,这里是逆序存储的。

行数据存储格式是这样的:

0x03 0x02 0x05 null值列表 头字段 hello, ni, hao, a, b

NULL值列表

null值列表,说的就是一行数据里可能有的字段是选填的,值可以是null。比如name字段,如果允许为null,那么实际存储的时候,要标记出来。

为了节省存储空间,MySQL设计的时候,使用二进制的bit位来存储null值列表。

假如创建了一张表:

CREATE TABLE user (
    name VARCHAR(10)NOT NULL,
    address VARCHAR(20),
    gender CHAR(1),
    job VARCHAR(30),
    school VARCHAR(50)
)ROW_FORMAT = COMPACT;

user表5个字段,4个变长的,只有name是非NULL的,其他的4个字段都是可以为NULL的。

假如现在插入一行数据:zhangsan NULL M NULL Tsinghua,address和job是NULL,那么它在磁盘上是怎么存储的?

NULL值列表,是这样存储的,你所有允许值为NULL的字段,都会有一个二进制的bit值,bit值为1说明是NULL,如果bit值为0说明不是NULL。

上面插入的那行数据,address、gender、job、school4个字段都允许为NULL,每个字段都会有一个bit位,其中address与job是NULL,所有4个bit位应该是:1010。

而且NULL值列表也是逆序存储的,所以NULL值列表里是0101。

一般NULL值列表是8个bit位的倍数,如果不足8个bit位,则高位补0。

所以现在看来,行数据存储格式是这样的:

0x08 0x08 00000101 头信息 col1的值,col2的值,col3的值......

数据头以及真实数据

行数据的头长度是固定的40bit,第一个bit和第二个bit,都是预留的,暂时没有用到。

记录头信息详细如下表所示,有的暂时没有用到,可以暂时不用深究它。

名称

大小 (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

表示下一条记录的相对位置

现在再加上数据头部分,上面的行存储格式就变成这样了,

0x08 0x08 00000101 0000000000000000000010000000000000011001 zhangsan M Tsinghua

刚开始先是变长字段的长度,用16进制表示,然后是NULL值列表,标识哪些值是NULL,接着是40bit的数据头,最后是真实数据。

在读取数据的时候,也会根据变长字段的长度,先读取出长度为8的zhangsan。

然后发现第二个字段是NULL,就不用再读了。

第三个字段是定长的1,就直接读取出gender为M。

第四个字段是NULL,就不用再读了。

第五个字段变长,长度是8,再读取长度为8的Tsinghua。

然而,真正磁盘上存储的时候,那些字符串就是直接存储在磁盘上的吗?

实际上,字符串都是根据数据库指定的字符集编码,进行编码之后再存储的,编码后大概是这样的:

0x08 0x08 00000101 0000000000000000000010000000000000011001 341324 134546 9342345

大家会看到上面,字符串和其他类型的数值最终都会根据数据库字符集编码,变成一些数字和符号存储在磁盘上。

最后,在实际存储一行数据的时候,MySQL还会给每条记录,加入一些隐藏字段。如下表:

列名

是否必须

占用空间 (bit)

描述

DB_ROW_ID

6

行ID,唯一标识一条记录

DB_TRX_ID

6

事务ID

DB_ROLL_PTR

7

回滚指针

如果用户没有指定主键,且表中没有Unique键时才会使用DB_ROW_ID作为主键。

最终,上面那条数据的存储格式,变成这样了:

0x08 0x08 00000101 0000000000000000000010000000000000011001 00000000034C(DB_ROW_ID)00000000036D(DB_TRX_ID) EA000010022B(DB_ROL_PTR) 341324 134546 9342345

到这里为止,我们基本把一行数据在磁盘上是如何存储的讲清楚了。

END

如果你喜欢本文,

请长按二维码,关注 南山的架构笔记

图片