15.10 InnoDB记录格式

一张表的记录格式决定了这张表的记录在物理上是怎样存储的,而后者又会影响查询和DML操作的性能。单个磁盘页面可以容纳的记录越多,查询和索引查找就会更快,buffer pool需要的缓冲区内存就会更少,写入被更新的记录所消耗的I/O也更少。

每张表中的数据按页存储。组成每张表的多个页被组织成两个一树形数据结构,被称为B+树索引。表数据和二级索引都使用这种类型的数据结构。表示整张表的B+树索引被称为聚簇索引,依照主键顺序组织。聚簇索引数据结构的节点包含了记录中的所有列的值。一个二级索引结构的节点中包含了索引列和主键列的值。

对于列的值存储在B+树索引中这个规则来说,变长字段是一个例外。过长的变长字段无法存储在B+树索引中,会被存储在独立分配的磁盘页面中(溢出页)。这种列被称为off-page列。off-page列的值被存储在一个单向链表中。每一个列有自己独立链表,包含一个或更多溢出页。整个变长列或者前缀被存储在B+树中,可以避免浪费存储空间,从而不用去读取溢出页,这取决于列的长度。

InnoDB存储引擎支持4种记录格式:REDUNDANT,COMPACT,DYNAMIC,和COMPRESSED。

表15.15 InnoDB记录格式概览

记录格式紧凑存储加强变长列存储支持大索引键前缀支持压缩支持的表空间类型
REDUNDANT系统,独立,通用
COMPACT系统,独立,通用
DYNAMIC系统,独立,通用
COMPRESSED独立,通用

下面介绍记录格式存储特性,以及怎样定义和确定一张表的记录格式。

REDUNDANT Row Format #

REDUNDANT格式与旧版本的MySQL兼容。

使用REDUNDANT记录格式的表将变长字段(varchar,varbinary,以及blob和text)的前768个字节存储在B+树节点的索引记录中,剩余部分存储在溢出页中。长度达到或超过768字节的定长列被编码成变长列。例如,如果相关字符集每个字符的最大长度可以超过3,一个char(255)的列,可以存储的长度超过768个字节,比如utf8mb4。

如果一个列的值没有超过768个字节,就不会用到溢出页,也可以节省一些I/O(因为整个值被存储在B+树节点中)。对相对较短的BLOB列来说很好,但是可能会导致B+树节点被数据充满而不是键的值,这降低了性能。如果表中有很多BLOB列,会导致B+树节点变得太满,包含的记录却太少,和都是短记录的表或列值被存储在溢出页的表相比,这使得整个索引效率不高。

REDUNDANT记录格式的存储特性 #

REDUNDANT记录格式的存储特性如下:

  • 每条索引记录都包含一个6字节长的header。这个header被用来连接相连的记录,以及用于记录锁。
  • 聚簇索引中的记录包含所有用户定义的列。此外,含包含一个6字节的事务id和一个7字节的回滚指针。
  • 如果一张表没有定义主键,每一个聚簇索引都会包含一个6字节的记录id字段。
  • 每条二级索引记录包含所有的主键列。
  • 每条记录包含一个指向这条记录中的字段的指针。如果这些字段的总长度不足128字节,这个指针是1字节长。否则,这个指针是2字节长。指针列表被称为记录目录。指针指向的区域被称为记录的数据部分。
  • 在内部,定长列(比如char(10))被按定长格式存储。而varchar列尾部的空余空间不会被清空(???)。
  • 长度大于或等于768字节的定长列被编码成变长列,可以被存储在溢出页中。例如,如果相关字符集的一个字符的最大长度超过3,一个char(255)的列可以存储的长度超过768个字节,比如utf8mb4。
  • 在记录目录中为NULL值保留一个或两个字节。如果被存储在变长列中,一个SQL中的NULL值在记录的数据部分占用0字节。对于一个定长列来说,这个列的固定长度被存储在记录的数据部分。位null值保留的固定空间使得当列被修改为non-null时,可以就地更新,而不会导致索引页碎片。

COMPACT Row Format #

和REDUNDANT记录格式相比,COMPACT记录格式占用的存储空间降低了约20%,代价是某些操作的CPU使用增加了。如果你的典型工作负载收到缓存命中率和磁盘速度的限制,COMPACT格式可能更快。如果典型工作负载受到CPU速度的限制,COMPACT记录格式可能会更慢。

使用COMPACT记录格式的表将变长列的前768字节存储在索引记录中,剩余部分存储在溢出页中。长度大于等于768字节的列会被编码成变长列。例如如果相关字符集中每个字符占用的最大字节数会超过3,一个char(255)的列可以存储的长度超过768字节,比如utf8mb4。

如果某个列的长度小于或等于768字节,不会用到溢出页,所有数据被存储在B+树节点中,可以节省一些I/O。这对短BLOB列来说没什么,但是会导致B+树节点充满数据,而不是键值,这会降低效率。使用很多BLOB列的表会导致B+树节点会导致B+树节点变得太满,包含的记录太少,和短记录或被存储在溢出页的表相比,这使得整个索引效率不高。

COMPACT记录格式的存储特性 #

COMPACT记录格式的存储特性如下:

  • 每条索引记录包含一个5字节长的header,其前面可能有变长字段信息。这个header倍用来链接连续的记录,并用于记录锁。

  • 记录header中的变长长度部分包含一个位向量,用来标识null列。如果索引中可以是null列的数量是N,那么这个位向量的长度是ceiling(N/8)字节(例如,如果有9-16列都可以是null,那么位向量使用两个字节)。除了位向量中的位,值是null的列不占用存储空间。header的变长字段长度部分也包含变长列的长度。每一个列的长度占1-2字节,具体值取决于这一列的最大长度。如果索引中的所有列都是not null并且是定长的,记录header就没有变长列长度部分。

  • 对每一个non-null变长字段,记录的header中用1-2字节记录这一列的长度。只有当某一列被存储在溢出页中,或是最大长度超过255字节且实际长度超过127字节,才需要两个字节来记录长度。对于一个存储在溢出页中的列,2字节的长度用来记录页内存储的那一部分数据长度+20字节的指针(指向溢出页)。业内存储部分是768字节,所以长度是768+20.这20字节的指针中也存储这这一列的真实长度。

  • 记录header后存储的是non-null列的数据内容。

  • 聚簇索引中的记录包含所有用户定义的列。此外,还有一个6字节的事务id和7字节的回滚指针。

  • 如果一张表没有定义主键,每一个聚簇索引记录会包含一个6字节的记录id字段。

  • 每一个二级索引记录都包含所有的主键列。如果任意一个主键列是变长的,每条二级索引记录的header部分就会有一部分记录变长列的长度,即使这个二级索引中的列是定长列。

  • 在内部,对于一个非变长结果集,定长字符列,比如char(10),按照定长格式存储。 变长列尾部的空间不会被清空(??)。

  • 在内部,对于诸如utf8mb3和utf8mb4一类的变长结果集,InnoDB会去除尾部的空间,尝试用N字节存储char(N)。如果一个char(N)列的值的长度超过N字节,尾部空白会被去除以使这一列占用的空间最小。一个char(N)列的最大长度是单个字符的最大字节数*N。

    会为CHAR(N)列保留最少N字节。这可以在大多数场景中保证就地更新,避免产生索引页面碎片。与此形成对比的是,使用REDUNDANT格式时,一个CHAR(N)列占用的空间是字符最大长度*N。

    长度大于或等于768字节的定长列被编码成变长列,可以存储在溢出页中。例如,如果使用的字符集中每个字符的最大长度大于3,比如utf8mb4,一个char(255)类型的列,存储长度可以超过768字节。

DYNAMIC Row Format #

DYNAMIC格式提供的存储特性和COMPACT格式相同,只是增加了对于长变长列的存储能力和存储大索引键前缀的能力。

如果创建一张表时指定的记录格式是DYNAMIC,InnoDB就可以讲长变长字段的所有内容都存储在溢出页中,聚簇索引中只包含20字节的指针。长度大于等于768字节的定长字段被编码成变长字段。例如,如果字符集中单个字符的最大长度大于3,比如utf8mb4,那么char(255)类型的列可以存储的长度超过768字节。

相关列是否被存储在溢出页中,取决于页大小和记录的总长度。如果一条记录过长,最长的列就会被存储在溢出页中,直到B+树索引页面可以容纳这条记录。长度小于或等于40字节的TEXT和BLOB列被存储在页内。

DYNAMIC格式保留了只要可以容纳,就将整条记录存储在索引节点中的高效,但是避免了B+树节点被大量长列的数据充满的问题。DYNAMIC记录格式基于的观点是:与其将一个长数据值存储在溢出页中,还不如将整个值存储在溢出页中,这样更高效。对于DYNAMIC格式,短列更有可能被存储在B+树节点中,需尽量减少一条记录所需的溢出页数量。

DYNAMIC格式支持的最大索引键长度是3072字节。

使用DYNAMIC格式的表可以被存储在系统表空间中,独立表空间中和通用表空间中。要将一张DYNAMIC表存储在系统表空间中,禁用innodb_file_per_table并使用一个常规的CREATE TABLE货ALTER TABLE语句,或在CREATE TABLE即ALTER TABLE语句后加上TABLESPACE [=] innodb_system选项。innodb_file_per_table变量不可用于通用表空间,当使用TABLESPACE [=] innodb_system选项来在系统表空间存储DYNAMIC表时,也无效。

DYNAMIC记录格式的存储特性 #

DYNAMIC格式是COMPACT格式的一个变种。要查看其存储特性,请查阅COMPACT记录格式的存储特性

COMPRESSED Row Format #

COMPRESSED格式提供了和DYNAMIC格式相同的存储特性,只是增加了对表和索引数据压缩的支持。

COMPRESSED使用了和DYNAMIC相同的溢出页存储技术,只是出于对存储和性能考虑,增加了对表和索引数据压缩的功能,可以使用更小的页面。使用COMPRESSED记录格式,KEY_BLOCK_SIZE选项控制着多少数据被存储在聚簇索引中,多少数据被存储在溢出页页中。想了解更多关于COMPRESSED记录格式的信息,请查阅15.9,“InnoDB表和页面压缩”

COMPRESSED格式支持的最大索引长度是3072字节。

使用COMPRESSED格式的表可以被存储在独立表空间或通用表空间中。系统表空间不支持COMPRESSED格式。要在独立表空间中存储一张COMPRESSED表,必须启用innodb_file_per_table变量。innodb_file_per_table对通用表空间无效。通用表空间支持所有记录格式,只是压缩表和非压缩表不能存在同一个通用表空间中,因为物理页大小不同。想了解更多信息,请查阅15.6.3.3,“通用表空间”

COMPRESSED记录格式的存储特性 #

COMPRESSED格式是COMPACT格式的一个变种。要了解存储特性,请查阅COMPACT记录格式的存储特性

定义一张表的行格式 #

InnoDB使用的默认行格式是由变量innodb_default_row_format定义,默认值是DYNAMIC。如果不明确指明行格式,或使用ROW_FORMAT=DEFAULT,就会使用默认行格式。

一张表的行格式可以在alter table或create table语句中使用row_format选项来指定。例如:

CREATE TABLE t1 (c1 INT) ROW_FORMAT=DYNAMIC;

使用ROW_FORMAT显式指定行格式会覆盖默认行格式。指定row_format=default效果和不指定一样。

可以动态设置innodb_default_row_format变量:

mysql> SET GLOBAL innodb_default_row_format=DYNAMIC;

innodb_default_row_format可选项包括DYNAMI,COMPACT,REDUNDANT。系统表空间不支持的COMPRESSED格式,不能被定义为默认格式,它只能在CREATE TABLE或ALTER TABLE语句中显式指定。将innodb_default_row_format变量设置为COMPRESSED会导致错误:

mysql> SET GLOBAL innodb_default_row_format=COMPRESSED;
ERROR 1231 (42000): Variable 'innodb_default_row_format'
can't be set to the value of 'COMPRESSED'

当创建表时不使用ROW_FORMAT选项明确指定行格式时或使用ROW_FORMAT=DEFAULT时,会使用innodb_default_row_format变量定义的格式。例如,先的语句使用innodb_default_row_format变量定义的行格式:

CREATE TABLE t1 (c1 INT);
CREATE TABLE t2 (c1 INT) ROW_FORMAT=DEFAULT;

如果没有显式指定行格式,或使用了ROW_FORMAT=DEFAULT,重建表操作会默认将表的行格式修改为默认行格式。

重建表操作包括使用ALGORITHM=COPY或ALGORITHM=INPLACE选项的ALTER TABLE操作。见章节15.2.1,“Online DDL操作”。OPTIMIZE TABLE也是一个重建表操作。

下面的例子展示了一个默认修改表行格式的重建表操作:

mysql> SELECT @@innodb_default_row_format;
+-----------------------------+
| @@innodb_default_row_format |
+-----------------------------+
| dynamic |
+-----------------------------+
mysql> CREATE TABLE t1 (c1 INT);
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE 'test/t1' \G
*************************** 1. row ***************************
 TABLE_ID: 54
 NAME: test/t1
 FLAG: 33
 N_COLS: 4
 SPACE: 35
 ROW_FORMAT: Dynamic
ZIP_PAGE_SIZE: 0
 SPACE_TYPE: Single
mysql> SET GLOBAL innodb_default_row_format=COMPACT;
mysql> ALTER TABLE t1 ADD COLUMN (c2 INT);
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE 'test/t1' \G
*************************** 1. row ***************************
 TABLE_ID: 55
 NAME: test/t1
 FLAG: 1
 N_COLS: 5
 SPACE: 36
 ROW_FORMAT: Compact
ZIP_PAGE_SIZE: 0
 SPACE_TYPE: Single

在将一张格式为REDUNDANT或COMPACT的现有表更改为DYNAMIC格式之前,考虑一下潜在的问题:

  • REDUNDANT和COMPACT格式支持的最大索引键前缀是767字节,而DYNAMIC和COMPRESSED格式支持的最大索引键前缀是3072字节。

在主从复制场景中,如果源库上的默认行格式呗设置为DYNAMIC,而副本上的默认格式是COMPACT,下面的没有显示指定行格式的DDL语句,在源库上会执行成功,在副本上会执行失败:

CREATE TABLE t1 (c1 INT PRIMARY KEY, c2 VARCHAR(5000), KEY i1(c2(3070)));

想了解相关信息,请查阅章节[15.22,“InnoDB限制”]。

  • 如果源库和目标库的默认行格式不一致,导入一张表时不显示置顶行格式,会导致数据库不匹配的错误。想了解更多信息,请查阅15.1.6.3,“导入InnoDB表”

确定一张表的行格式 #

要确定一张表行格式,使用 show table status:

mysql> SHOW TABLE STATUS IN test1\G
*************************** 1. row ***************************
 Name: t1
 Engine: InnoDB
 Version: 10
 Row_format: Dynamic
 Rows: 0
 Avg_row_length: 0
 Data_length: 16384
Max_data_length: 0
 Index_length: 16384
 Data_free: 0
 Auto_increment: 1
 Create_time: 2016-09-14 16:29:38
 Update_time: NULL
 Check_time: NULL
 Collation: utf8mb4_0900_ai_ci
 Checksum: NULL
 Create_options:
 Comment:

你也可以查询INFORMATION_SCHEMA.TABLES表:

mysql> SELECT NAME, ROW_FORMAT FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME='test1/t1';
+----------+------------+
| NAME | ROW_FORMAT |
+----------+------------+
| test1/t1 | Dynamic |
+----------+------------+