【MySQL系列8】InnoDB中四种行格式分析及对varchar和char的限制

  • 举例3

  • 总结

[](()MySQL系列文章汇总

========================================================================

| MySQL系列文章 | 飞机票 |

| — | — |

| MySQL索引为何选择B+树 | [登机入口](() |

| 深入分析MySQL索引 | [登机入口](() |

| MySQL执行计划EXPLAIN详细说明和举例 | [登机入口](() |

| MySQL中事务以及MVCC的实现原理 | [登机入口](() |

| 深入分析MySQL中锁并详解幻读问题 | [登机入口](() |

| select语句和update语句的执行流程 | [登机入口](() |

| InnoDB引擎存储结构及Change Buffer和Double Writer分析 | [登机入口](() |

| InnoDB中四种行格式分析及对varchar和char的限制 | [登机入口](() |

[](()前言

===============================================================

上一篇介绍了[InnoDB存储引擎的整体存储结构]((),这次我们就再来深入分析下InnoDB中的表以及数据到底是怎么存储的。

本文基于MySQL5.7版本。

[](()InnoDB表逻辑存储结构

==========================================================================

前面我们介绍了,InnoDB中的数据都被存储在表空间(tabespace)中,而表空间又由段(segment),区(extent),页(page)组成,有时候页也被称为块(block)。大致结构如下图:

( 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 图片来源于《MySQL技术内幕:InnoDB存储引擎》)

在这里插入图片描述

[](()表空间

================================================================

默认情况有有一个共享表空间文件ibdata1,另外提供了一个参数innodb_file_per_table来控制是否开启单独表空间,这个参数默认是开启状态。

另外独占表空间存放表的索引和数据,其他数据如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。

[](()段


表空间是由各个segment(段)组成的,常见的有以下段:

  • 数据段,即:Leaf node segment。因为InnoDB是索引组织表,数据存在B+树的叶子节点

  • 索引段,即:Non-Leaf node segment。B+树中非叶子节点不存储数据,只是索引。

  • 回滚段,即:Rollback 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格式

REDUNDANT格式提供了与旧版本MySQL的兼容性。

每行存储结构如下:

| 字段长度偏移列表 | 头信息 | 列1 | 列2 | 列… |

| — | — | — | — | — |

[](()特性

REDUNDANT行格式具有如下特性:

  • 每个索引记录包含一个6字节的头信息。头信息用于将连续的记录链接在一起,并用于行级锁定。

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

  • 如果没有为表定义主键,则每个聚集索引记录还包含一个6字节的行ID字段。

  • 每个辅助索引记录包含不在辅助索引中的聚集索引键定义的所有主键列。

  • 一个记录包含一个指向记录中每个字段的指针。如记录中字段的总长度小于128字节,则指针为1字节;否则,2个字节。指针数组称为记录目录。指针所指向的区域是记录的数据部分。

  • 在内部,像CHAR(10)这样的固定长度字符列以固定长度格式存储。尾随空格不会被截断。

  • 将变长列值的前768字节存储在B+tree节点的索引记录中,其余的存储在溢出页中。大于或等于768字节的固定长度列被编码为可变长度列,可以在页外存储。例如,如果字符集的最大字节长度大于3(如:utf8mb4),CHAR(255)列可以超过768字节)。如果列的值为768字节或更少,则不会使用溢出页,因为该值完全存储在B+树节点中。

  • 可变长列中null值不占用空间,定长列中null值会占用定义的空间大小。定长列的空值保留固定空间是为了当列从空值更新为非空值时不会导致索引页碎片。

[](()COMPACT格式

与REDUNDANT行格式相比,COMPACT行格式减少了约20%的行存储空间,但会增加某些操作的CPU使用。

对于变长字段的存储方式和REDUNDANT一致。

每行存储结构如下(表头部分没有含义,仅为了便于后文描述):

| index1 | index2 | index3 | index4 | index5 | index6 |

| — | — | — | — | — | — |

| 变长字段长度列表 | NULL标志位 | 头信息 | 列1 | 列2 | 列… |

[](()特性

  • 每个索引记录包含一个5字节的头信息(index3),并且前面可能还会有一个可变长度信息(index1和inex2)。头信息用于将连续的记录链接在一起,并用于行级锁定。

  • 上图中index1部分最多2个字节。如果列的总长度小于255个字节,则index1部分用1个字节表示;如果列的长度大于255个字节,则inex1部分用2个字节表示。这是因为在InnoDB中varchar类型最大长度为65535,所以用2个字节表示就够了。

  • 如果一行中有N个列允许为Null,则null标志位占用N/8个字节,也就是说,每8个Null列,就会占用1个字节.

  • 如果一个列中没有Null值也没有可变长度列,上面的index1和index2就会没有(index1和index2同属于一个可变长头信息部分)。

  • 将变长列值的前768字节存储在B+tree节点的索引记录中,其余的存储在溢出页中。

  • 对于外部存储的列,2字节长度表示内部存储部分的长度加上指向溢出部分的20字节指针。内部部分是768字节,所以长度是768+20。20字节的指针存储列的真实长度。

  • 上图中index4开始,存储的是非空列信息。

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

  • 如果没有为表定义主键,则每个聚集索引记录还包含一个6字节的行ID字段。

  • 每个辅助索引记录包含不在辅助索引中的聚集索引键定义的所有主键列。如果任何一个主键列是可变长度的,那么每个辅助索引的头信息都有一个可变长度的部分来记录它们的长度,即使辅助索引定长列。

  • 在内部,对于不可变长字符集,像CHAR(10)这样的固定长度字符列以固定长度格式存储。尾随空格不会被截断。

  • 在内部,对于可变长度的字符集,如utf8和utf8mb4, InnoDB尝试通过去除尾部空格将CHAR(N)存储为N个字节。如果CHAR(N)列值的字节长度超过N个字节,则会通过去除尾部空格来达到列值字节长度的最小值。CHAR(N)列的最大长度是最大字符字节长度乘以N(如uft8,CHAR(10)列最大长度为3*10)。

  • 为CHAR(N)保留最小的N个字节。在多数情况下,保留最小空间可以在更新的时候不会导致索引页碎片。相比之下,在使用REDUNDANT行格式时,CHAR(N)列占用的最大字符字节长度乘以N(如utf8下,char(10)保留10个字节,而REDUNDANT会保留3*10个字节)。

[](()DYNAMIC格式

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格式

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限制

=====================================================================

  • 一个表最多可以包含1017列(在MySQL 5.6.9中从之前的1000列限制提高了)。虚拟生成的列包括在这个限制中。

  • 一个表最多可以包含64个二级索引。

  • 如果innodb_large_prefix是启用的(默认),对于使用DYNAMIC或COMPACT行格式的InnoDB表,索引键前缀限制为3072字节。如果innodb_large_prefix被禁用,对于任何行格式的表,索引键前缀限制为767字节。

innodb_large_prefix已被弃用,将在未来的版本中被删除。innodb_large_prefix是在MySQL 5.5中引入的,用于禁用大索引键前缀,以与不支持大索引键前缀的InnoDB早期版本兼容。

如果在创建MySQL实例时通过指定innodb_page_size选项将InnoDB页面大小减少到8KB或4KB,索引键的最大长度将按比例降低:3072字节限制基于16KB页面大小。也就是说,当页面大小为8KB时,最大索引键长度为1536字节,而当页面大小为4KB时,最大索引键长度为768字节。

  • 适用于索引键前缀的限制也适用于全列索引键。

  • 多列索引最多允许16列。

  • 对于4KB、8KB、16KB和32KB的页,最大行大小(不包括存储在页外的任何可变长度的列)应略小于页大小的一半。然而对于64KB的页来说,最大行大小约为1/4页。

  • LONGBLOB和LONGTEXT列必须小于4GB,包括BLOB和TEXT列在内的总行大小也必须小于4GB。

  • 如果一行小于半页长,那么它全部存储在本地页中。如果它超过半页,那么将选择可变长度的列进行外部页外存储,直到行能适应半页的大小。

  • 虽然InnoDB内部支持大于65,535字节的行大小,但MySQL本身对所有列的合并大小施加了65,535的行大小限制

  • InnoDB日志文件的最大大小是512GB。

  • 最小表空间大小略大于10MB。最大表空间大小取决于InnoDB的页面大小(表空间的最大值也是表的最大值):

| 页大小 | 对应的表空间最大值 |

| — | — |

| 4KB | 16TB |


版权声明:本文为m0_67614284原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。