目录
MySQL的数据存放在哪个文件?
表空间文件的结构是怎么样的?
1、行(row)
2、页(page)
3、区(extent)
4、段(segment)
InnoDB 行格式有哪些?
COMPACT行格式长什么样?
记录的额外信息
1、变长字段长度列表
2、NULL值列表
3、记录头信息
记录的真实信息
varchar(n) 中 n 最大取值为多少?
单字段的情况
多字段的情况
行溢出后,MySQL是怎么处理的?
总结
MySQL存储的行为由存储引擎实现的,MySQL 支持多种存储引擎,不同的存储引擎保存的文件也不同。
InnoDB是我们常用的存储引擎,也是 MySQL 默认的存储引擎。所以本文主要围绕 InnoDB 存储引擎展开。
查询MySQL数据库的文件存放在哪个目录:
mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| datadir | /var/lib/mysql/ |
+---------------+-----------------+
1 row in set (0.00 sec)
我们每创建一个 database (数据库)都会在 datadir 目录里创建一个以 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
可以看到,公有三个文件,这三个文件分别代表着:
总结:每创建一张表都会生成.opt,.frm,.ibd三个文件,其中 .opt 存储字符集和字符校验规则; .frm 存储表结构定义;.ibd 存储每张表的数据。
表空间由 段(segment)、区(extent)、页(page)、行(row)组成,InnoDB存储引擎的逻辑结构大致如下图:
数据表中的记录都是按行(row)进行存放的,每个记录根据不同的行格式,有不同的存储结构。
记录是按照行来存储的,但是数据库的读取并不以 [行] 为单位,否则一次读取(也就是一次 I/O操作)只能处理一行数据,效率会非常低。
因此 InnoDB的数据是按 [页] 为单位读写的,也就是说,当需要读取一条记录的时候,并不是将这个记录从磁盘读出来,而是以页为单位,将其整体读入内存。
默认每个页的大小为 16 KB ,也就是最多能保证 16 KB 的连续存储空间。
页是 InnoDB 存储引擎磁盘管理的最小单元,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16 K 内容刷新到磁盘中。
页的类型很多,常见的有数据页、undo 日志页、溢出页等等。数据表中的页式用 [数据页] 来管理的。
InnoDB存储引擎是用 B+ 树来组织数据的。
B+ 树中每一层都是通过双向链表连接起来的,如果是以页为单位来分配存储空间,那么链表中相邻的两个页之间的物理位置并不是连续的,可能离得非常远,那么磁盘查询时就会有大量的随机I/O,随机 I/O 是非常慢的。
解决这个问题也很简单,就是让链表中相邻的页的物理位置也相邻,这样就可以使用顺序 I/O 了,那么在范围查询(扫描叶子结点)的时候性能就会很高。
解决办法:
在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB ,对于 16KB 的页来说,连续的 64 个页会被划分为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。
表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段、索引段和回滚段等。
行格式(row_format),就是一条记录的存储结构。
InnoDB 提供了 4 种行格式,分别是Redundant 、Compact、Dynamic和Compressed行格式。
可以看到,一条完整的记录分为 [记录的额外信息] 和 [记录的真实数据] 两个部分。
记录的额外信息包含三个部分:变长字段长度列表、NULL值列表、记录头信息。
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 表里有三条记录:
接下来我们看看这三条记录的行格式中的 [变长字段长度列表] 是怎么存储的?
第一条记录:
这些变长字段的真实数据占用的字节数会按照列的顺序逆序存放,所以 [变长字段长度列表] 里的内容是 [03 01],而不是 [01 03]。
同样,可以得出第二条记录的行格式中,[变长字段长度列表] 里的内容是 [04 02] ,如下图:
第三条记录中的 phone 的 列值为 NULL ,NULL 是不会存放在行格式中记录的真实数据部分里的,所以 [变长字段长度列表] 里不需要保存值为 NULL 的变长字段的长度。
为什么 [变长字段长度列表] 的信息要按照逆序存放?
主要是因为 [ 记录头信息] 中指向下一个记录的指针,指向的是下一条记录的 [记录头信息] 和 [真实数据] 之间的位置,这样的好处就是向左读就是记录头信息,向右读就是真实数据,比较方便。
[变长字段长度列表] 中的信息之所以要逆序存放,是因为这样可以使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。
同样的道理,NULL值列表的信息也需要逆序存放。
每个数据库表的行格式都有 [变长字段字节数列表] 吗?
变长字段字节数列表不是必须的。
当数据表没有变长字段的时候,比如全部都是 int 类型的字段,这时候表里的行格式就不会有 [变长字段长度列表] 了,因为没必要,不如去掉可以节省空间。
所以 [变长字段长度列表] 只出现在数据表中有变长字段的时候。
表中的某些列可能会存储 NULL值,如果把这些 NULL 值都放在记录的真实数据中会比较浪费空间,所以Compact 行格式把这些值为 NULL的列存储到 NULL 值列表中。
如果存在允许 NULL 值的列,则每一个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。
另外,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 字节吗?如果是这样的话,一条记录有九个字段值都是 NULL ,这时候怎么表示?
[NULL值列表] 的空间不是固定 1 字节的。
当一条记录有 9 个字段值都是 NULL ,那么就会创建 2 字节空间的 [NULL 值列表],以此类推。
记录头信息包含的内容很多,这里例举几个比较重要的:
记录的真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为 row_id、trx_id、roll_pointer。
MySQL规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(步包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节。
也就是说,一行记录除了TEXT、BLOBs 类型的列,限制最大为 65535 字节,注意是一行的总长度不是一列。
PS: varchar(n) 字段类型的 n 代表的是最多存储的字符数量,并不是字节大小。
要算varchar(n)最大能允许存储的字节数,还需要看数据库表的字符集,因为字符集代表着,1个字符占用多少字节,比如ascii字符集,1 个字符占用 1 字节,那么 varchar(100)意味着最大能允许存储 100 字节的数据。如果是utf-8字符集,一个中文字符占用 3 字节,英文占用 1 字节。
前面,我们知道了一行记录最大只能存储 65535 字节的数据
假设数据库表只有一个 varchar(n)类型的列且字符集是 ascii ,在这种情况下,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的,而且行字段数不超过8,所以会用 1 字节来表示 [NULL值列表]。
本例中,[变长字段长度列表] 所占用的字节数是多少?
[变长字段长度列表] 所占用的字节数 = 所有 [变长字段长度] 占用的字节数之和。
所以,我们要先知道每个变长字段的 [变长字段长度] 需要用多少字节表示?具体情况分为:
因为我们这个案例是只有 1 个变长字段,所以「变长字段长度列表」= 1 个「变长字段长度」占用的字节数,也就是 2 字节。
因为在算 varchar(n) 中 n 最大值时,需要减去 「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。所以,在数据库表只有一个 varchar(n) 字段且字符集是 ascii 的情况下,varchar(n) 中 n 最大值 = 65535 - 2 - 1 = 65532。
可以看到,创建成功了。所以在算 varchar(n) 中 n 最大值时,需要减去 「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。
当然,上面这个例子是针对字符集为 ascii 情况,如果采用的是 UTF-8,varchar(n) 最多能存储的数据计算方式就不一样了:
上面所说的只是针对于一个字段的计算方式。
如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535
MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB
,也就是 16384字节
,而一个 varchar(n) 类型的列最多可以存储 65532字节
,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的「溢出页」中。
如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。在一般情况下,InnoDB 的数据都是存放在 「数据页」中。但是当发生行溢出时,溢出的数据会存放到「溢出页」中
当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在 [溢出页] 中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。
上面这个是 Compact 行格式在发生行溢出后的处理。
Compressed 和 Dynamic 这两个行格式和 Compact 非常类似,主要的区别在于处理行溢出数据时有些区别。
这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中,看起来就像下面这样
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 最大取值为 65535(行最大存储) - 2(变长字段长度列表) - 1(NULL值列表) = 65532。
如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。
ps:在计算的时候需要考虑字符集、NULL值列表 和 变长字段列表 三部分。
行溢出后,MySQL是怎么处理的?
如果一个数据页存不了一条记录 ,InnoDB存储引擎会自动将溢出的数据存放在 [溢出页] 中
Compact 行格式:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。
Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。