上一篇介绍了InnoDB存储引擎的整体存储结构,这次我们就再来深入分析下InnoDB中的表以及数据到底是怎么存储的。
本文基于MySQL5.7版本。
前面我们介绍了,InnoDB中的数据都被存储在表空间(tabespace)中,而表空间又由段(segment),区(extent),页(page)组成,有时候页也被称为块(block)。大致结构如下图:
(图片来源于《MySQL技术内幕:InnoDB存储引擎》)
默认情况有有一个共享表空间文件ibdata1,另外提供了一个参数innodb_file_per_table来控制是否开启单独表空间,这个参数默认是开启状态。
另外独占表空间存放表的索引和数据,其他数据如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。
表空间是由各个segment(段)组成的,常见的有以下段:
段又是由不同的区(extent)组成
对于16KB或者小于16KB大小的页(page)来说,区的大小固定为1MB(64个连续的16KB页面,或128个8KB页面,或256个4KB页面)。
对于32KB的页面大小,区段大小为2MB。
对于64KB的页面大小,区段大小为4MB。
InnoDB中默认页大小为16KB,可以由变量innodb_page_size修改:
值 | 描述 |
---|---|
4096 | 4KB或者4K |
8192 | 8KB或者8K |
16384 | 16KB或者16K。默认页的大小 |
32768 | 32KB或者32K(MySQL5.7之后) |
65536 | 64KB或者64K(MySQL5.7之后) |
注意:innodb_page_size是一个global变量,只能在初始化MySQL实例的时候进行设置,中途不能修改,如果没有设置,则会采用默认大小16KB。
创建一个表:
CREATE TABLE `my_user` (
`id` varchar(1350) DEFAULT NULL,
`name` varchar(1350) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
再看一下页的大小:
然后去服务器上查看一下表空间大小:
我们发现,大小只有96KB,但是上面又说页的大小在16KB的时候,一个区就是64个页,那么默认大小应该至少有1MB才对,但是这里为什么只有96KB呢?
这是因为InnoDB为了防止小表过多而占用了太多空间,所以默认情况下会有32个碎片页来分配,初始的时候会分配6个碎片页来存储一些必要信息,然后在需要空间的时候继续按需分配,直到32个碎片页分配完了之后,就会开始整个完整的区开始进行分配。
注意:分配区的时候并不总是单个区来分配,有时候为了保证数据的连续性,InnoDB可能会一次性将4个区加载到段中。
InnoDB存储引擎是面向行的(row-oriented),也就是说数据是按行进行存放的,每个页存放的行记录也是有硬性定义的,最多允许存放16KB/2-200,即7992行记录。
每行数据允许的最大长度应略小于半个page大小。例如,对于默认的16KB InnoDB页面大小,最大行长度略小于8KB。对于64KB的页面,最大行长度略小于16KB。
这么设计的原因是为了更好地利用B+树的特性,如果一个页只能存一行数据,那么整个B+树的叶子节点就相当于一个链表,无法很好地利用B+树的特性。想要详细了解B+树特性的,可以点击这里。
表的行格式决定了它的行是如何物理存储的,这反过来又会影响查询和DML操作的性能。当一个磁盘页中可以容纳更多的行时,查询和索引查找可以工作得更快,缓冲池中需要的缓存内存更少,写入更新值所需的I/O也更少。
我们知道,B+树的数据就存储的叶子节点,但是可变长度列(如varchar,text,blob等)是一个例外。如果变长列太长而无法装入B+树页,则存储在单独分配的磁盘页上(溢出页)。这些列称为页外列,页外列的值存储在溢出页的单独链接列表中,每个这样的列都有自己的一个或多个溢出页列表。根据列长度的不同,所有变长列值或前缀都存储在B+树中,以避免浪费存储空间并不得不读取单独的页面。
行格式主要分为四种:REDUNDANT, COMPACT, DYNAMIC, and COMPRESSED。
REDUNDANT格式提供了与旧版本MySQL的兼容性。
每行存储结构如下:
字段长度偏移列表 | 头信息 | 列1 | 列2 | 列… |
---|
REDUNDANT行格式具有如下特性:
与REDUNDANT行格式相比,COMPACT行格式减少了约20%的行存储空间,但会增加某些操作的CPU使用。
对于变长字段的存储方式和REDUNDANT一致。
每行存储结构如下(表头部分没有含义,仅为了便于后文描述):
index1 | index2 | index3 | index4 | index5 | index6 |
---|---|---|---|---|---|
变长字段长度列表 | NULL标志位 | 头信息 | 列1 | 列2 | 列… |
DYNAMIC格式提供了和COMPACT相同的存储特征,但为比较长的可变长度的列增加了增强的存储能力,并支持大型索引键前缀。
当用ROW_FORMAT=DYNAMIC创建一个表时,InnoDB可以在完全脱离页面的情况下存储长列的可变长度值(对于VARCHAR、VARBINARY、BLOB和TEXT类型),而聚集索引记录只包含一个指向溢出页面的20字节指针。大于或等于768字节的固定长度字段被编码为可变长度字段。
列是否在页外存储取决于页大小和行的总大小。如果行太长,则选择最长的列进行页外存储,直到聚集索引记录适合B+tree页为止。小于或等于40字节的文本和BLOB列存储在行中。
DYNAMIC行格式最多支持3072字节的索引键前缀。这个特性由innodb_large_prefix变量控制,该变量在默认情况下是启用的。
DYNAMIC行格式保持了在索引节点中存储整个行的效率(REDUNDANT和COMPACT格式也是如此),但DYNAMIC行格式避免了用大量长列的数据字节填充B+树节点的问题。
动态行格式基于这样一种思想:如果长数据值的一部分存储在页外,那么在页外存储整个值通常是最有效的。使用DYNAMIC格式,较短的列可能保留在B+树节点中,从而最小化给定行所需的溢出页数。
其他的特性均与COMPACT行格式一致。
COMPRESSED行格式提供了与DYNAMIC行格式相同的存储特性和功能,但增加了对表和索引数据压缩的支持。
COMPRESSED行格式使用与DYNAMIC行行格式类似的实现来进行页外存储,同时还考虑了被压缩的表和索引数据的额外存储和性能考虑,并使用更小的页大小。
对于压缩行格式,KEY_BLOCK_SIZE选项控制有多少列数据存储在聚集索引中,以及有多少列数据存储在溢出页面上。
COMPRESSED行格式最多支持3072字节的索引键前缀。这个特性由innodb_large_prefix变量控制,该变量在默认情况下是启用的。
其余特性与COMPACT行格式一样。
InnoDB表的默认行格式是由innodb_default_row_format变量定义的,该变量的默认值为DYNAMIC。如果没有显式定义ROW_FORMAT表选项,或者指定了ROW_FORMAT= default,则使用默认的行格式。
SET GLOBAL innodb_default_row_format=DYNAMIC;
注意:有效的innodb_default_row_format选项包括DYNAMIC,COMPACT, 和REDUNDANT。因为system表空间不支持使用COMPACT行格式,因此不能将其定义为缺省值。只能在CREATE TABLE或ALTER TABLE语句中显式地指定它。试图设置innodb_default_row_format变量为COMPACT会报错:
SET GLOBAL innodb_default_row_format=COMPRESSED;
ERROR 1231 (42000): Variable 'innodb_default_row_format'
can't be set to the value of 'COMPRESSED'
另外可以使用CREATE table或ALTER table语句中的ROW_FORMAT表选项显式定义表的行格式。例如:
CREATE TABLE t1 (c1 INT) ROW_FORMAT=DYNAMIC;
如果一张表的行格式需要从REDUNDANT或COMPACT改成DYNAMIC或COMPACT,需要注意的是:
REDUNDANT和COMPACT行格式支持最大索引键前缀长度为767字节,而DYNAMIC和COMPRESSED行格式支持最大索引键前缀长度为3072字节,尤其是主从复制时尤其要注意,如果主从设置的行格式不一致,可能导致一方的语句执行失败。
innodb_large_prefix已被弃用,将在未来的版本中被删除。innodb_large_prefix是在MySQL 5.5中引入的,用于禁用大索引键前缀,以与不支持大索引键前缀的InnoDB早期版本兼容。
如果在创建MySQL实例时通过指定innodb_page_size选项将InnoDB页面大小减少到8KB或4KB,索引键的最大长度将按比例降低:3072字节限制基于16KB页面大小。也就是说,当页面大小为8KB时,最大索引键长度为1536字节,而当页面大小为4KB时,最大索引键长度为768字节。
页大小 | 对应的表空间最大值 |
---|---|
4KB | 16TB |
8KB | 32TB |
16KB | 64TB |
32KB | 126TB |
64KB | 256TB |
MySQL的硬限制是每个表有4096列,但是对于给定的表,有效的最大值可能更少。确切的列限制取决于几个因素:
给定表的最大行大小由下面几个因素决定:
下面的InnoDB和MyISAM示例演示了MySQL最大行大小限制为65,535字节。无论存储引擎是什么,都会强制执行此限制,即使存储引擎可能能够支持更大的行:
InnoDB:
mysql> CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000),
c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000),
f VARCHAR(10000), g VARCHAR(6000)) ENGINE=InnoDB CHARACTER SET latin1;
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
MyISAM:
mysql> CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000),
c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000),
f VARCHAR(10000), g VARCHAR(6000)) ENGINE=MyISAM CHARACTER SET latin1;
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
注意:这两个例子中要注意的是编码用的是latin1,如果用的是utf8则大小要除以3,用的是utf8mb4,大小要除以4,后面的例子中也是一样。
上面两个例子中,如果将最后一列g改为text则可以执行成功:
InnoDB:
mysql> CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000),
c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000),
f VARCHAR(10000), g TEXT(6000)) ENGINE=InnoDB CHARACTER SET latin1;
Query OK, 0 rows affected (0.02 sec)
MyISAM:
mysql> CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000),
c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000),
f VARCHAR(10000), g TEXT(6000)) ENGINE=MyISAM CHARACTER SET latin1;
Query OK, 0 rows affected (0.02 sec)
虽然最大限制为65535,但是实际上每一列还要2个字节来存储大小,所以实际上能存储的是65535-(列数*2),注意这个前提是没有其他列:
mysql> CREATE TABLE t1
(c1 VARCHAR(32765) NOT NULL, c2 VARCHAR(32766) NOT NULL)
ENGINE = InnoDB CHARACTER SET latin1;
Query OK, 0 rows affected (0.02 sec)
上面这个加起来是65535-(2*2)=65531,如果列可以为NULL那么长度每8列还要再减1。
下面这个建立一个65535的列(65533就可以成功)是失败的:
mysql> CREATE TABLE t2
(c1 VARCHAR(65535) NOT NULL)
ENGINE = InnoDB CHARACTER SET latin1;
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
上面举的都是变长varchar的例子,这里再来一个定长char的例子:
mysql> CREATE TABLE t4 (
c1 CHAR(255),c2 CHAR(255),c3 CHAR(255),
c4 CHAR(255),c5 CHAR(255),c6 CHAR(255),
c7 CHAR(255),c8 CHAR(255),c9 CHAR(255),
c10 CHAR(255),c11 CHAR(255),c12 CHAR(255),
c13 CHAR(255),c14 CHAR(255),c15 CHAR(255),
c16 CHAR(255),c17 CHAR(255),c18 CHAR(255),
c19 CHAR(255),c20 CHAR(255),c21 CHAR(255),
c22 CHAR(255),c23 CHAR(255),c24 CHAR(255),
c25 CHAR(255),c26 CHAR(255),c27 CHAR(255),
c28 CHAR(255),c29 CHAR(255),c30 CHAR(255),
c31 CHAR(255),c32 CHAR(255),c33 CHAR(255)
) ENGINE=InnoDB ROW_FORMAT=COMPACT DEFAULT CHARSET latin1;
ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB or using
ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED may help. In current row format, BLOB prefix of 768
bytes is stored inline.
如果把上面的例子中ENGINE换成MYISAM就会成功,这是为什么呢?
原因上面介绍过了,因为InnoDB引擎限制了一行最大也应略小于页的一半。16KB的一半应该是8192,除去一些其他信息占用的空间和提示中的8126差不多能匹配上,而建表语句中总长度为:255*33=8415,已经超过一半了,故而报错。
本文主要内容来自于官网的翻译,比较偏向于理论性质,这篇文章最主要目的是为了将MySQL及InnoDB的一些特性和限制记录下来,以便后续复习这一块的时候可以更加方便,文章中的部分理论本人并未去实践操作,本文中的内容均基于MySQL5.7版本,如有其他版本,可能会有少许偏差,如上文中默认建表后的表空间大小,用MySQL8.0.21版本建表后,默认大小为112KB而不是96KB。
下一篇MySQL系列文章,将会介绍MySQL中的数据类型相关知识。
请关注我,和孤狼一起学习进步。