2021-06-05 11:27  阅读(1542)
文章分类:MySQL 技术内幕 文章标签:MySQLMySQL 技术内幕MySQL 高级
©  原文作者:一直不懂 原文地址:https://blog.csdn.net/shenchaohao12321/category_8075653.html

InnoDB存储引擎和大多数数据库一样(如 Oracle和 Microsoft SQL Server数据库),记录是以行的形式存储的。这意味着页中保存着表中一行行的数据。在 InnoDB1.0.x版本之前, InnoDB存储引擎提供了 Compact和 Redundant两种格式来存放行记录数据,这也是目前使用最多的一种格式。 Redundant格式是为兼容之前版本而保留的,如果阅读过 InnoDB的源代码,用户会发现源代码中是用PHYSICAL RECORD( NEW STYLE)和 PHYSICAL RECORD( OLD STYLE)来区分两种格式的。在 MySQL5.1版本中,默认设置为 Compact行格式。用户可以通过命令 SHOW TABLE STATUS LIKE' table_name'来查看当前表使用的行格式,其中 row format属性表示当前所使用的行记录结构类型。如:

        mysql> SHOW TABLE STATUS like 'mytest'\G;

1、Compact行记录格式

Compact行记录是在 MySQL5.0中引入的,其设计目标是高效地存储数据。简单来说,一个页中存放的行数据越多,其性能就越高。

202106051127305801.png
可以观察到, Compact行记录格式的首部是一个非NULL变长字段长度列表,并且其是按照列的顺序逆序放置的,其长度为:

  • 若列的长度小于255字节,用1字节表示;
  • 若大于255个字节,用2字节表示

变长字段的长度最大不可以超过2字节,这是因在 MySQL数据库中 VARCHAR类型的最大长度限制为65535。变长字段之后的第二个部分是NULL标志位,该位指示了该行数据中是否有NULL值,有则用1表示。该部分所占的字节应该为1字节。接下来的部分是记录头信息(record header),固定占用5字节(40位),每位的含义见下表。

名称 大小(bit) 描述
名称 大小(bit) 描述
() 1 未知
() 1 未知
deleted_flag 1 该行是否已被删除
min_rec_flag 1 为1,如果该记录是预先被定义为最小的记录
n_owned 4 该记录拥有的记录数
heap_no 13 索引堆中该条记录的排序记录
record_type 3 记录类型,000表示普通,001表示B+树节点指针,010表示Infimum,011表示Supremum,1xx表示保留
next_record 16 页中下一条记录的相对位置
total 40  

最后的部分就是实际存储每个列的数据。需要特别注意的是,NULL不占该部分任何空间,即NULL除了占有NULL标志位,实际存储不占有任何空间。另外有一点需要注意的是,每行数据除了用户定义的列外,还有两个隐藏列,事务ID列和回滚指针列,分别为6字节和7字节的大小。若 InnoDB表没有定义主键,每行还会增加一个6字节的 rowid列。
接下去用一个具体示例来分析 Compact行记录的内部结构:
202106051127311622.png

202106051127323753.png

在上述示例中,创建表 mytest,该表共有4个列。t1、t2、t4都为 VARCHAR变长字段类型,t3为固定长度类型CHAR。接着插入了3条有代表性的数据,然后将打开表空间文件 mytest.ibd(这里启用了 innodb_file_per_table,若没有启用该选项,打开默认的共享表空间文件(ibdata1)。
在 Windows操作系统下,可以选择通过程序 UltraEdit打开该二进制文件。在 Linux环境下,使用命令

        hexdump -C-v mytest.ibd > mytest.txt

这里将结果重定向到了文件mytest. txt,打开 mytest. txt文件,找到如下内容:

202106051127328954.png

该行记录从0000c078开始,若整理一下,相信用户会有更好的理解:

202106051127339235.png

现在第一行数据就展现在用户眼前了。需要注意的是,变长字段长度列表是逆序存放的,因此变长字段长度列表为03 02 01,而不是01 02 03。此外还需要注意 InnoDB每行有隐藏列 Transaction和 Roll Pointer。同时可以发现,固定长度CHAR字段在未能完全占用其长度空间时,会用0x20来进行填充。
接着再来分析下 Record header的最后两个字节,这两个字节代表 next_recorder,0x2c代表下一个记录的偏移量,即当前记录的位置加上偏移量0x2c就是下条记录的起始位置。所以 InnoDB存储引擎在页内部是通过一种链表的结构来串连各个行记录的。
第二行将不做整理,除了 RowID不同外,它和第一行大同小异。

现在来关心有NULL值的第三行:
202106051127346406.png

第三行有NULL值,因此NULL标志位不再是00而是06,转换成二进制为00000110,为1的值代表第2列和第3列的数据为NULL。在其后存储列数据的部分,用户会发现没有存储NULL列,而只存储了第1列和第4列非NULL的值。因此这个例子很好地说明了:不管是CHAR类型还是 VARCHAR类型,在 compact格式下NULL值都不占用任何存储空间。

2、Redundant行记录格式

Redundant是 MySQL5.0版本之前 InnoDB的行记录存储方式,MSQL5.0支持Redundant是为了兼容之前版本的页格式。

3、行溢出数据

InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外。一般认为BLOB、LOB这类的大对象列类型的存储会把数据存放在数据页面之外。但是,这个理解有点偏差,BLOB可以不将数据放在溢出页面,而且即便是 VARCHAR列数据类型,依然有可能被存放为行溢出数据。
首先对VARCHAR数据类型进行研究。很多DBA喜欢 MySQL数据库提供的VARCHAR类型,因为相对于 Oracle VARCHAR2最大存放4000字节, SQL Server最大存放8000字节, MySQL数据库的VARCHAR类型可以存放65535字节。但是,这是真的吗?真的可以存放65535字节吗?如果创建 VARCHAR长度为65535的表,用户会得到下面的错误信息:

        CREATE TABLE test(
            -> a VARCHAR(65535)
            -> )CHARSET=latin1 ENGINE=InnoDB;
        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
    

从错误消息可以看到 InnoDB存储引擎并不支持65535长度的 VARCHAR。这是因为还有别的开销,通过实际测试发现能存放VARCHAR类型的最大长度为65532。例如,按下面的命令创建表就不会报错了。

        mysql> CREATE TABLE test( a VARCHAR(65532) )CHARSET=latin1 ENGINE=InnoDB;
        Query OK, 0 rows affected (0.04 sec)

需要注意的是,如果在执行上述示例的时候没有将SQL_MODE设为严格模式或许可以建立表,但是 MySQL数据库会抛出一个 warning,如:

202106051127352577.png

warning信息提示了这次可以创建是因为 MySQL数据库自动地将 VARCHAR类型转换成了TEXT类型。查看test的表结构会发现:

202106051127359798.png

还需要注意上述创建的 VARCHAR长度为65532的表,其字符类型是 latin1的,如果换成GBK又或UTF8的,会产生怎样的结果呢?

        mysql> create table test(
            -> a varchar(65532)
            -> )charset=gbk engine=InnoDB;
        ERROR 1074 (42000): Column length too big for column 'a' (max = 32767); use BLOB or TEXT instead
        mysql> create table test(
            -> a varchar(65532)
            -> )charset=utf8 engine=InnoDB;
        ERROR 1074 (42000): Column length too big for column 'a' (max = 21845); use BLOB or TEXT instead

这次即使创建列的 VARCHAR长度为65532,也会提示报错,但是两次报错对max值的提示是不同的。因此从这个例子中用户也应该理解 VARCHAR(N)中的N指的是字符的长度。而文档中说明 VARCHAR类型最大支持65535,单位是字节。
此外需要注意的是, MySQL官方手册中定义的65535度是指所有 VARCHAR列的长度总和,如果列的长度总和超出这个长度,依然无法创建,如下所示:

        mysql> create table test2(
            -> a varchar(22000),
            -> b varchar(22000),
            -> c varchar(22000)
            -> )charset=latin1 engine=InnoDB;
        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

3个列长度总和是66000,因此 InnoDB存储引擎再次报了同样的错误。即使能存放65532个字节,但是有没有想过, InnoDB存储引擎的页为16KB,即16384字节,怎么能存放65532字节呢?因此,在一般情况下, InnoDB存储引擎的数据都是存放在页类型为B-tree node中。但是当发生行溢出时,数据存放在页类型为 Uncompress BLOB页中。来看下面一个例子:
202106051127364689.png

在上述例子中,首先创建了一个列a长度为65532的 VARCHAR类型表t,然后插入了列a长度为65532的记录,接着通过工具py_innodb_page_info看表空间文件,可以看到的页类型有:

2021060511273705210.png

2021060511273821911.png

通过工具可以观察到表空间中有一个数据页节点B-tree Node,另外有4个未压缩的二进制大对象页 Uncompressed BLOB Page,在这些页中才真正存放了65532字节的数据。既然实际存放的数据都在BLOB页中,那数据页中又存放了些什么内容呢?同样通过之前的 hexdump来读取表空间文件,从数据页c000开始查看:

2021060511273854112.png

可以看到,从0x0000c093到0x0000392数据页面其实只保存了 VARCHAR(65532)的前768字节的前缀(prefix)数据(这里都是a),之后是偏移量,指向行溢出页,也就是前面用户看到的 Uncompressed BLOB Page。因此,对于行溢出数据,其存放采用下图的方式。

2021060511274110313.png

那多长的 VARCHAR是保存在单个数据页中的,从多长开始又会保存在BLOB呢?可以这样进行思考: InnoDB存储引擎表是索引组织的,即B+Tree的结构,这样每个页中至少应该有两条行记录(否则失去了B+Tree意义,变成链表了)。因此,如果页中只能存放下一条记录,那么 InnoDB存储引擎会自动将行数据存放到溢出页中。考虑下面表的一种情况:
2021060511274132314.png

表t变长字段列a的长度为9000,故能存放在一个数据中,但是这并不能保证两条长度为9000的记录都能存放在一个页中。
但是,如果可以在一个页中至少放入两行数据,那 VARCHAR类型的行数据就不会存放到BLOB页中去。经过多次试验测试,发现这个阈值的长度为8098
另一个问题是,对于TEXT或BLOB的数据类型,用户总是以为它们是存放在Uncompressed BLOB Page中的,其实这也是不准确的。是放在数据页中还是BLOB页中,和前面讨论的 VARCHAR一样,至少保证一个页能存放两条记录。

当然既然用户使用了BLOB列类型,一般不可能存放长度这么小的数据。因此在大多数的情况下BLOB的行数据还是会发生行溢出,实际数据保存在BLOB页中,数据页只保存数据的前768字节。

4、Compressed和 Dynamic行记录格式

InnoDB 1.0.x版本开始引入了新的文件格式( file format,用户可以理解为新的页格式),以前支持的 Compact和 Redundant格式称为 Antelop文件格式,新的文件格式称为 Barracuda文件格式。 Barracuda文件格式下拥有两种新的行记录格式: Compressed和Dynamic新的两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式,如图所示,在数据页中只存放20个字节的指针,实际的数据都存放在 Off Page中,而之前的 Compact和 Redundant两种格式会存放768个前缀字节。
2021060511274210415.png
Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zib的算法进行压缩,因此对于BLOB、TEXT、 VARCHAR这类大长度类型的数据能够进行非常有效的存储。

5、CHAR的行结构存储

通常理解VARCHAR是存储变长长度的字符类型,CHAR是存储固定长度的字符类型。而在前面的小节中,用户已经了解行结构的内部的存储,并可以发现每行的变长字段长度的列表都没有存储CHAR类型的长度。
然而,值得注意的是之前给出的两个例子中的字符集都是单字节的 latin1格式。从MySQL4.1版本开始,CHAR(N)中的N指的是字符的长度,而不是之前版本的字节长度。
也就说在不同的字符集下,CHAR类型列内部存储的可能不是定长的数据。例如下面的这个示例:

        mysql> create table j(a char(2))charset=utf8 engine=InnoDB;
        Query OK, 0 rows affected (0.03 sec)
    
        mysql> insert into j select 'ab';
        Query OK, 1 row affected (0.01 sec)
        Records: 1  Duplicates: 0  Warnings: 0
    
        mysql> set names utf8;
        Query OK, 0 rows affected (0.00 sec)
    
        mysql> insert into j select '我们';
        Query OK, 1 row affected (0.02 sec)
        Records: 1  Duplicates: 0  Warnings: 0
    
        mysql> insert into j select 'a';
        Query OK, 1 row affected (0.00 sec)
        Records: 1  Duplicates: 0  Warnings: 0

在上述例子中,表j的字符集是UTF-8。用户分别插入了两个字符的数据'ab’和'我们',然后查看所占字节,可得如下结果:

        mysql> select a,char_length(a),length(a) from j;
        +--------+----------------+-----------+
        | a      | char_length(a) | length(a) |
        +--------+----------------+-----------+
        | ab     |              2 |         2 |
        | 我们   |              2 |         6 |
        | a      |              1 |         1 |
        +--------+----------------+-----------+
        3 rows in set (0.00 sec)

通过不同的 CHAR LENGTH和CHAR函数可以观察到:前两个记录'ab'和'我们’字符串的长度都是2。但是内部存储上'ab’占用2字节,而’我们'占用6字节。
如果通过HEX函数查看内部十六进制的存储,可以看到:

        mysql> select a,hex(a) from j;
        +--------+--------------+
        | a      | hex(a)       |
        +--------+--------------+
        | ab     | 6162         |
        | 我们   | E68891E4BBAC |
        | a      | 61           |
        +--------+--------------+
        3 rows in set (0.01 sec)

因此对于多字节的字符编码,CHAR类型不再代表固定长度的字符串了。例如,对于UTF8下CHAR(10)类型的列,其最小可以存储10字节的字符,而最大可以存储30字节的字符。因此,对于多字节字符编码的CHAR数据类型的存储, InnoDB存储引擎在内部将其视为变长字符类型。这也就意味着在变长长度列表中会记录CHAR数据类型的长度。

上述例子清楚地显示了 InnoDB存储引擎内部对CHAR类型在多字节字符集类型的存储。CHAR类型被明确视为了变长字符类型,对于未能占满长度的字符还是填充0x20。
InnoDB存储引擎内部对字符的存储和我们用HEX函数看到的也是一致的。因此可以认为在多字节字符集的情况下,CHAR和VARCHAR的实际行存储基本是没有区别的。


来源:https://blog.csdn.net/shenchaohao12321/category_8075653.html

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 【MySQL技术内幕】18-InnoDB行记录格式
上一篇
【MySQL技术内幕】17-InnoDB逻辑存储结构
下一篇
【MySQL技术内幕】19-InnoDB数据页结构