
目录一、InnoDB 行格式数据准备二、COMPACT行格式整体说明三、记录的额外信息一变长字段长度列表数据结构存储过程读取过程变长字段长度列表存储示例二NULL 值位图数据结构存储过程读取过程NULL 值位图示例说明三行头信息基本定义分析案例分析四、隐藏列一基本说明二主键的选择顺序说明三案例分析五、记录真实数据主要参考和学习来源干货分享感谢您的阅读先分享一个真实的案例某大型电商平台在一次促销活动中遭遇了数据库性能瓶颈通过优化 InnoDB 的行格式他们将查询性能提升了30%存储成本降低了20%。这不仅帮助他们顺利度过了高峰期还大大提升了用户体验。查询性能提升通过选择适当的行格式如 Compact 或 Dynamic可以减少存储开销和提升数据访问速度从而加快查询响应时间。存储成本降低压缩行格式如 Compressed可以显著减少磁盘空间的使用特别是在处理大量冗长字符串或重复数据时。想象一下你正在设计一个需要处理海量数据的应用从用户信息到交易记录每一行数据的存储方式都会直接影响到你的系统响应速度和存储成本。那么如何选择最合适的行格式来最大化性能和效率呢本次我们聚焦 InnoDB 行格式理解它们是如何在幕后悄悄发挥作用的。行格式的设计反映了数据库设计者在权衡性能、存储和兼容性时的决策。到现在为止一共设计了4种不同类型的行格式 分别是 Compact 、 Redundant 、Dynamic 和 Compressed 行格式随着时间的推移他们可能会设计出更多的行格式但是不管怎么变在原理上大体都是相同的。我们本次主要针对Compact InnoDB 行格式进行分析理解。一、InnoDB 行格式数据准备在 MySQL 中数据是以记录为单位插入到表中的而这些记录在磁盘上的存放方式就是我们所说的“行格式”或者“记录格式”。首先我们来看一下如何在创建或修改表时指定行格式。我们可以使用CREATE TABLE或ALTER TABLE语句来指定行格式。其语法如下CREATE TABLE 表名 (列的信息) ROW_FORMAT行格式名称; ALTER TABLE 表名 ROW_FORMAT行格式名称;假设我们在名为xiaohaizi的数据库中创建一个名为record_format_demo的表并指定它的行格式为 Compact同时设置字符集为 ASCIIASCII 字符集只包括空格、标点符号、数字、大小写字母和一些不可见字符所以我们的汉字是不能存到这个表里的。如下所示向这个表中插入两条记录并查看插入结果在实际应用中选择合适的行格式可以显著提升数据库的性能和存储效率。例如对于读多写少的场景Compressed 行格式可能是一个不错的选择而对于写操作频繁的场景Compact 行格式可能会更合适。因各原理上大体都是相同所以我们下面针对Compact进行理解。二、COMPACT行格式整体说明Compact 行格式适用于大多数通用场景尤其是需要高效存储和读取的小型至中型表。它提供了良好的性能和平衡的存储效率是 InnoDB 存储引擎中的默认选择。Compact 行格式在物理存储上采用以下结构行头信息用于存储事务信息和回滚指针占用 5 个字节。NULL 值位图用于标识哪些列是 NULL 值每个列对应 1 个 bit。变长字段长度列表紧跟在 NULL 值位图之后记录变长字段的长度信息。隐藏列每行有 6 个字节用于两个隐藏的系统列包括事务 ID 和回滚指针。实际数据存储实际的数据值紧凑排列。三、记录的额外信息一变长字段长度列表在 InnoDB 存储引擎的 Compact 行格式中变长字段长度列表用于存储变长字段的长度信息比如VARCHAR(M) 、 VARBINARY(M) 、各种 TEXT 类型各种 BLOB 类 型。通过这种方式Compact 行格式能够高效地管理和存储变长字段的数据。由于变长字段的长度是不固定的InnoDB 需要一种方式来记录和读取这些字段的实际长度以便正确地存取数据。数据结构每个变长字段占用 1 到 2 个字节长度小于 255 字节的字段使用 1 个字节来存储长度信息而长度等于或大于 255 字节的字段使用 2 个字节来存储长度信息。具体来说如果字段的长度小于 255 字节则使用 1 个字节表示其长度。如果字段的长度大于或等于 255 字节则使用 2 个字节表示其长度。存储过程计算每个变长字段的实际长度对于每个变长字段计算其实际长度。根据长度决定字节数如果长度小于 255则使用 1 个字节存储长度否则使用 2 个字节存储长度。存储长度信息将长度信息按顺序存储在变长字段长度列表中。存储实际数据紧跟在变长字段长度列表之后存储实际的数据值。读取过程读取变长字段长度列表首先读取变长字段长度列表获取每个变长字段的长度信息。根据长度信息读取数据根据变长字段长度列表中的长度信息准确定位和读取每个变长字段的实际数据值。变长字段长度列表存储示例针对之前创建的compact_format_demo表和插入的数据进行分析针对第一条插入的数据 aaaa, bbb, cc, dc1字段值为 aaaa长度为 4占用 1 个字节表示长度。c2字段值为 bbb长度为 3占用 1 个字节表示长度。c3字段值为 cc长度为 2占用 1 个字节表示长度。c4字段值为 d长度为 1占用 1 个字节表示长度。针对第二条插入的数据 eeee, fff, NULL, NULLc1字段值为 eeee长度为 4占用 1 个字节表示长度。c2字段值为 fff长度为 3占用 1 个字节表示长度。c3字段为 NULL不需要额外的长度信息。c4字段为 NULL不需要额外的长度信息。变长字段长度列表是按照字段顺序紧跟在 NULL 值位图之后存储的。对于第一条记录长度列表为 [4][3][2][1]占用了 4 个字节。对于第二条记录长度列表为 [4][3]占用了 2 个字节。总的长度列表占用了 6 个字节。二NULL 值位图在 InnoDB 存储引擎的 Compact 行格式中NULL 值位图用于标识每个字段是否为 NULL 值。在 InnoDB 存储引擎中NULL 值不占用实际的存储空间因此需要一种方式来标识哪些字段是 NULL以便在读取数据时正确处理这些字段。数据结构每个字段占用 1 个 bit位图中的每个 bit 对应一列用于标识该列是否为 NULL 值。位图中的 bit 排列顺序按照字段在表中的顺序依次排列从左到右。存储过程遍历每个字段对于每个字段检查其是否为 NULL 值。设置对应位图中的 bit如果字段为 NULL 值则将对应位图中的 bit 设置为 1否则将其设置为 0。位图的实际存储位图中的 bit 按照字段的顺序依次存储每个 bit 占用 1 位。读取过程读取 NULL 值位图首先读取 NULL 值位图获取每个字段是否为 NULL 值的信息。根据位图读取数据根据位图中的信息准确读取每个字段的数据值。如果对应位图中的 bit 为 1则表示该字段为 NULL 值否则读取实际的数据值。NULL 值位图示例说明还是针对之前创建的compact_format_demo表和插入的数据进行分析对于第一条插入的数据 (aaaa, bbb, cc, d)c1字段的值为 aaaa不是 NULL 值。c2字段的值为 bbb不是 NULL 值。c3字段的值为 cc不是 NULL 值。c4字段的值为 d不是 NULL 值。NULL 值位图为 [0][0][0][0]表示所有字段均不为 NULL。对于第二条插入的数据 (eeee, fff, NULL, NULL)c1字段的值为 eeee不是 NULL 值。c2字段的值为 fff不是 NULL 值。c3字段的值为 NULL是 NULL 值。c4字段的值为 NULL是 NULL 值。NULL 值位图为 [0][0][1][1]表示c3和c4字段为 NULL而c1和c2字段不为 NULL。三行头信息在 InnoDB 存储引擎中每个记录都有一个记录头信息它由固定的 5 个字节40 个二进制位组成。这 5 个字节中的每一位都有特定的含义描述了记录的一些重要信息。基本定义分析每个记录的开头有一个记录头信息这些信息包含了对记录的描述和控制。以下是每个二进制位代表的详细信息预留位11 bit该位暂时未被使用。预留位21 bit该位暂时未被使用。delete_mask1 bit标记该记录是否被删除。如果被删除则该位为 1否则为 0。min_rec_mask1 bitB树的每层非叶子节点中的最小记录都会添加该标记。如果是最小记录则该位为 1否则为 0。n_owned4 bits表示当前记录拥有的记录数。使用 4 个 bits 来表示可以表示的最大值为 15。heap_no13 bits表示当前记录在记录堆中的位置信息。使用 13 个 bits 来表示可以表示的最大值为 8191。record_type3 bits表示当前记录的类型。0普通记录。1B树非叶子节点记录。2最小记录。3最大记录。next_record16 bits表示下一条记录相对于当前记录的位置。使用 16 个 bits 来表示可以表示的最大值为 65535。这些记录头信息的二进制位提供了有关记录的详细描述包括了是否被删除、记录的拥有数量、位置信息等。理解这些信息有助于更好地理解 InnoDB 存储引擎中记录的存储和组织方式以及对数据库的性能和功能的影响。案例分析我们来分析一下compact_format_demo表中插入的第二条记录 (eeee, fff, NULL, NULL)的记录头信息分析先整理理论依据delete_mask用于标记记录是否被删除。min_rec_mask用于标记是否是 B 树非叶子节点中的最小记录。n_owned表示当前记录拥有的记录数。heap_no表示当前记录在记录堆中的位置信息。record_type表示当前记录的类型包括普通记录、B 树非叶子节点记录、最小记录和最大记录。next_record表示下一条记录相对于当前记录的位置。现在可以进行如下推断对于delete_mask和min_rec_mask根据描述如果满足描述条件则为 1否则为 0。对于n_owned在这个例子中没有其他相关的记录所以这个值应该是 0。对于heap_no插入的第二条记录应该在记录堆的第二个位置因此其二进制表示应该是 00000000000010。对于record_type根据描述这是一个普通记录所以这个值应该是 0。对于next_record因为这是最后一条记录所以下一条记录的相对位置应该是 0。综上所述我们可以得出插入的第二条记录的记录头信息应该是delete_mask: 0 min_rec_mask: 0 n_owned: 0 heap_no: 2 record_type: 0 next_record: 0四、隐藏列一基本说明了解记录的真实数据以外还有一些隐藏列由MySQL自动添加到每个记录中这些列包括row_id行ID用于唯一标识一条记录。在InnoDB表中如果用户没有定义主键也没有定义Unique键则InnoDB会为表默认添加一个名为row_id的隐藏列作为主键。这个列的存在意味着即使没有显式定义主键每条记录仍然有一个唯一的标识符。transaction_id事务ID用于标识执行此次数据操作的事务。每个事务都有一个唯一的事务ID这有助于数据库跟踪和管理事务的执行顺序以及处理并发事务之间的冲突。roll_pointer回滚指针用于实现多版本并发控制MVCC机制。回滚指针记录了事务开始时行的旧版本的位置以便在需要时回滚事务或查询历史数据。二主键的选择顺序说明提及row_id涉及到主键的生成策略时InnoDB表遵循一定的规则来确定主键的选择顺序。具体如下用户自定义主键首先InnoDB会优先选择用户自定义的主键作为表的主键。如果用户已经显式地定义了一个列作为主键那么这个列将被用作表的主键。Unique键作为主键如果用户没有定义主键但定义了一个Unique键唯一索引那么InnoDB会将这个Unique键作为表的主键。这样做是为了确保每条记录都有一个唯一的标识符。默认主键row_id如果表中既没有用户自定义的主键也没有定义Unique键那么InnoDB会为表默认添加一个名为row_id的隐藏列作为主键。这个列是InnoDB内部生成的用于确保每条记录都有一个唯一的标识符。三案例分析对于第二条插入的数据 (eeee, fff, NULL, NULL)事务ID每个事务都有一个唯一的事务ID表示执行此次数据操作的事务。对于第二条插入的记录我们假设事务ID为 T2。回滚指针回滚指针用于实现多版本并发控制MVCC机制记录了事务开始时行的旧版本的位置。对于第二条插入的记录我们假设回滚指针为 RP2。因此插入的第二条记录的隐藏列值可能如下所示事务IDT2占用 6 个字节回滚指针RP2占用 6 个字节这些隐藏列的值是由InnoDB存储引擎自动生成的对于用户来说是不可见的支持事务管理和并发控制。五、记录真实数据记录的真实数据是指用户自定义的列数据即在表中定义的可见列的值。在compact_format_demo表中可见列包括c1、c2、c3和c4。对于第二条插入的记录 (eeee, fff, NULL, NULL)其真实数据如下c1eeeec2fffc3NULLc4NULL这些值是用户插入的数据它们对于数据库来说是可见的可以通过查询操作检索到。与隐藏列不同这些数据由用户直接提供并且在数据库中占据着特定的列位置。因为表 record_format_demo 并没有定义主键所以 MySQL 服务器会为每条记录增加上述的3个列。现在看一下加上 记录的真实数据 的两个记录长什么样吧看这个图的时候我们需要注意几点表 record_format_demo 使用的是 ascii 字符集所以 0x61616161 就表示字符串 aaaa 0x626262 就表 示字符串 bbb 以此类推。注意第1条记录中 c3 列的值它是 CHAR(10) 类型的它实际存储的字符串是 cc 而 ascii 字符集中 的字节表示是 0x6363 虽然表示这个字符串只占用了2个字节但整个 c3 列仍然占用了10个字节的空 间除真实数据以外的8个字节的统统都用空格字符填充空格字符在 ascii 字符集的表示就是 0x20 。注意第2条记录中 c3 和 c4 列的值都为 NULL 它们被存储在了前边的 NULL值列表 处在记录的真实数据处 就不再冗余存储从而节省存储空间。主要参考和学习来源《MySQL 是怎样运行的从根儿上理解 MySQL》https://dev.mysql.com/doc/refman/5.7/en/https://dev.mysql.com/doc/internals/en/http://www.orczhou.com/https://blog.jcole.us/innodb/