18.InnoDB 存储引擎(存储结构)
1.InnoDB简介
1.MySQL为什么默认使用InnoDB存储引擎?
InnoDB在设计时考虑到了处理巨⼤数据量时的性能,InnoDB⽀持事务(transaction)、回滚
(rollback)并且具有崩溃修复的能⼒(crash recovery capabilities),通过多版本并发控制(multi
versioned concurrency control)减少锁定,同时还⽀持外键约束(FOREIGN KEY constraints),通
过缓冲池在主内存中缓存数据从⽽提⾼查询性能,也可以每个表使⽤各⾃的独⽴表空间存储数据并
且⽂件⼤⼩只受限于操作系统,由于InnoDB存储引擎存储数据量⼤,性能⾼,可以有效的保证数
据安全等优点,在MySQL5.5版本之后成为默认的存储引擎
衍生:
1. InnoDB事务是什么?
2. 事务的回滚是什么?
3. InnoDB崩溃修复如何处理?
4. 多版本并发控制是什么?
5. 锁分类都有哪些?
6. 缓冲池的作⽤及⼯作原理?
7. 独⽴表空间的作⽤及优点?
2.InnoDB存储引擎架构长啥样

InnoDB主要包括内存结构和磁盘结构
内存结构包括:
◦ 缓冲池(Buffer Pool) -- 内存中的主要工作区域,优化查询的性能
◦ 变更缓冲区(Change Buffer) -- 优化修改操作的性能
◦ ⽇志缓冲区(Log Buffer)
◦ ⾃适应哈希(Adaptive Hash Index) -- 进一步提升查询的性能
磁盘结构包括:
◦ 系统表空间(System Tablespace)
◦ 独⽴表空间(File-Per-Table Tablespaces)
◦ 通⽤表空间(General Tablespaces)
◦ 临时表空间(Temporary Tablespaces)
◦ 撤销表空间(Undo Tablespaces)
◦ 重做⽇志(Undo Log)
◦ 双写缓冲区 (Doublewrite Buffer)
3.为什么要设计成内存结构和磁盘结构两个部分
• 我们从MySQL实现的⻆度来思考这个问题,数据库的作⽤就是保存数据,⽤⼾的真实数据最终都会保存在磁盘上,在查询数据的过程中,如果每次都从磁盘上读取会严重影响效率,为了提⾼数据的访问效率,InnoDB会把查询到的数据缓存到内存中,当再次查询时,如果⽬标数据已经存在于内存中,就可以从内存中直接读取,从⽽⼤幅提升效率。
• 也就是说磁盘结构中的⽂件是⽤来保存数据实现数据持久化的,内存结构是⽤来缓存数据提升效率的

4.使⽤InnoDB存储引擎创建的表对应的数据⽂件在哪⾥?
• ⽆论使⽤哪个存储引擎创建表,真实的数据都必须存储在磁盘或其他存储介质中。
• 当使⽤InnoDB存储引擎创建⼀个表时,默认会在数据⽬录对应的数据库⼦⽬录中⽣成相应的表空
间⽂件,以 .ibd 为⽂件的后缀,⽤来存储数据和索引,如果每个表都对应⼀个表空间⽂件,称为
独⽴表空间,在MySQL5.7及以后的版本中默认为每个表⽣成独⽴表空间,可以通过系统变量
innodb_file_per_table[=ON|OFF] 进⾏控制,如果关闭这个选项,则所有表的数据都在
系统表空间中存储,独⽴表空间⽂件如下所⽰:
root@guangchen-vm:/var/lib/mysql/test_db# ll # 列出指定数据库⽬录下的所有⽂件
total 640
drwxr-x--- 2 mysql mysql 4096 10⽉ 30 11:51 ./
drwxr-x--- 8 mysql mysql 4096 11⽉ 5 11:15 ../
# .ibd⽂件是使⽤InnoDB存储引擎创建的表对应的表空间⽂件
-rw-r----- 1 mysql mysql 114688 9⽉ 6 15:30 classes.ibd
-rw-r----- 1 mysql mysql 114688 9⽉ 6 15:30 course.ibd
-rw-r----- 1 mysql mysql 114688 9⽉ 6 15:30 score.ibd
-rw-r----- 1 mysql mysql 114688 9⽉ 6 15:30 student.ibd2.MySQL存储结构
(MySQL为了有效的维护数据而定义的一系列数据结构)
1.什么是表空间文件?
• 表空间⽂件是⽤来存储表中数据的⽂件,表空间⽂件的⼤⼩由存储的数据多少决定,不同的表空间⽂件存储数据的种类也有所不同,在MySQL中表空间分为五类,包括:系统表空间、独⽴表空间、通⽤表空间、临时表空间和撤销表空间,这些在上⾯的InnoDB架构图中都有体现。
表空间文件与表空间的关系是神么
表空间可以理解为MYSQL为了管理数据⽽设计的⼀种数据结构,主要描述的对结构的定义,表空间⽂件是对定义的具体实现,以⽂件的形式存在于磁盘上
2.⽤⼾数据在表空间中是怎么存储的?
• ⾸先明确⼀点,⽤⼾的数据以数据⾏的⽅式存储在对应的表空间⽂件中,那么表空间中很多个数据⾏就需要进⾏管理,以便后续进⾏⾼效的查询;
• 为了⽅便管理,表空间由段 (segment)、区组(group)、区 (extent)、⻚ (page) 、数据⾏组成,
其中⻚是 InnoDB 磁盘管理的最⼩单位;
• 可以这么理解,若⼲数据⾏组成了⻚,多个⻚组成了区,多区组成了区组,多个区组组成了段,多个段组成了表空间。
3.为什么使用页这个数据来管理单元?
• ⾸先要明确⼀点,MySQL中的⻚是应⽤层的⼀个概念,是MySQL根据⾃⾝的应⽤场景,定义的⼀种数据结构。
• 通常操作系统中的⽂件系统在管理磁盘⽂件时以4KB⼤⼩为⼀个管理单元,称为"数据块",但是在数据库的应⽤场景⾥,查询时数据量都⽐较⼤,如果也使⽤4KB做数据存储的最⼩的单元,就显的有点⼩了,同时会造成频繁的磁盘I/O,导致降低效率;
• 所以MySQL根据⾃⾝情况定义了⼤⼩为16KB的⻚,做为磁盘管理的最⼩单位;
• 每次内存与磁盘的交互⾄少读取⼀⻚,所以在磁盘中每个⻚内部的地址都是连续的,之所以这样
做,是因为在使⽤数据的过程中,根据局部性原理,将来要使⽤的数据⼤概率与当前访问的数据在
空间上是临近的,所以⼀次从磁盘中读取⼀⻚的数据放⼊内存中,当下次查询的数据还在这个⻚中
时就可以从内存中直接读取,从⽽减少磁盘I/O,提⾼性能
• MySQL根据⾃⾝的应⽤场景使⽤⻚做为数据管理单元,最主要的⽬的就是减少磁盘I/O,提⾼性
能。
什么是局部性原理?
局部性原理是指程序在执⾏时呈现出局部性规律,在⼀段时间内,整个程序的执⾏仅限于程序中
的某⼀部分。相应地,执⾏所访问的存储空间也局限于某个内存区域,局部性通常有两种形式:时间局部性和空间局部性。
时间局部性(Temporal Locality):如果⼀个信息项正在被访问,那么在近期它很可能还会被再
次访问。
空间局部性(Spatial Locality):将来要⽤到的信息⼤概率与正在使⽤的信息在空间地址上是临
近的。
4.数据⻚有哪些基本特性是必须要掌握的?
• ⻚的16KB的⼤⼩是MySQL的⼀个默认设置,可以适⽤于⼤多数场景,当然也可以根据⾃⼰的实际业务场景进⾏修改⻚的⼤⼩,通过系统变量 innodb_page_size 进⾏调整与查看,在调整⻚⼤
⼩的时候需要保证设置的值是操作系统"数据块" 4KB的整数倍,从⽽保证通过操作系统和磁盘交互
时"数据块"的完整性,不被分割或浪费,所以规定了 innodb_page_size 可以设置的值,分别
是 4096 、 8192 、 16384 、 32768 、 65536 ,对应 4KB 、 8KB 、 16KB 、 32KB 、64KB ;
• 每⼀个⻚中即使没有数据也会使⽤ 16KB 的存储空间,同时与索引的B+树中的节点对应。查看⻚的⼤⼩,可以通过系统变量 innodb_page_size 查看
mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 | # 16KB
+------------------+-------+
1 row in set, 1 warning (0.04 sec)在不同的使⽤场景中,⻚的结构也有所不同,在MySQL中有多种不同类型的⻚,但不论哪种类型的⻚都会包含⻚头(File Header)和⻚尾(File Trailer),在这⻚头和⻚尾之间的⻚主体信息根据不同的类型有不同的结构,最常⽤的就是⽤来存储数据和索引的"索引⻚",也叫做"数据⻚",⻚的主体信息使⽤数据"⾏"进⾏填充,⻚的基本结构如下图所⽰:

5.查询的数据超过一页的大小,怎么提高查询效率?-- 区
不同的⻚在磁盘中是不是连续的呢?
• 答案是不⼀定,在不做任何控制的情况下,不同⻚在磁盘中申请的地址⼤概率是不连续的。
• 我们可以很快的分析出来连续的地址对查询效率的影响,如果⻚在磁盘中可以被连续读取,那么查询效率就⾼,否则果询效率就低。
为什么不连续的地址会降低查询的效率?
• 当存储介质是机械硬盘时,访问不连续的地址会带来磁盘寻址的开销,也就是磁头在不同盘⾯、磁道和扇区的机械转动,这个过程称为磁盘随机访问,⾮常影响效率,磁盘结构如下图所⽰:


InnoDB如何保证⻚在磁盘中的连续性?
• 为了解决磁盘随机访问⾮常低效的问题,需要尽可能在磁道上读取连续的数据,减少磁头的移动,从⽽提升效率,MySQL使⽤ Extent(区) 这个结构来管理⻚,规定每个区固定⼤⼩为 1MB ,可以存放 64 个⻚,这时如果跨⻚读数据时,⼤概率都在附近的地址,可以⼤幅减少碰头移动;
提⽰:区的固定⼤⼩不⽤刻意去记,现阶段是1MB,以后的版本会不会改变也说不好。
• 同时,如果频繁的读取某个区中的⻚,可以把整个区都读取出来放⼊内存中,减少后续查询对磁盘的访问次数,进⼀步提升效率,如图所⽰

通过对问题的分析,我们了解到InnoDB中⽤来组织⻚的数据结构--区,并且每个区固定⼤⼩为
1MB ,可以包含64个连续的⻚,查询的数据超过⼀⻚⼤⼩时,可能会有以下⼏种情况:
a. ⻚在区内是相邻的:磁盘顺序I/O,可以⼤幅提升效率
b. ⻚在区内但不是相邻的:可以⼤幅减少碰头移动,可以提升效率
c. ⻚在不同的区:还是要发⽣随机I/O,不能提升效率
6.那么⼜有⼀个问题来了,新创建表时没有数据,或者说有的表只有很少的数据,1MB的空间⽤不完,那不是就存在空间浪费的问题吗?
• 当创建表时,并不知道当前表的数据量级
• 为了节省空间,最初只创建7个初始⻚(在MySQL5.7中创建6个初始⻚),⽽不是⼀个完整的区,可以通过以下SQL查看:
mysql> select * FROM information_schema.INNODB_TABLESPACES WHERE name =
'test_db/student'\G
*************************** 1. row ***************************SPACE: 13NAME: test_db/studentFLAG: 16417ROW_FORMAT: DynamicPAGE_SIZE: 16384 # ⻚⼤⼩ZIP_PAGE_SIZE: 0SPACE_TYPE: SingleFS_BLOCK_SIZE: 65536FILE_SIZE: 114688 # 数据⽂件初始⼤⼩ALLOCATED_SIZE: 114688
AUTOEXTEND_SIZE: 0SERVER_VERSION: 8.0.33SPACE_VERSION: 1ENCRYPTION: NSTATE: normal
1 row in set (0.01 sec)# 根据数据⽂件⼤⼩和每⻚⼤⼩计算出⻚数
# 114688 / 16384 = 7 个数据⻚这些零散⻚会放在表空间中⼀个叫碎⽚区的区域,随着数据量的增加,会申请新的⻚来存储数据,
当碎⽚区达到32个⻚的时候,后续每次都会申请⼀个完整的区来存储更多的数据;
7.如果访问的数据跨区了怎么办?区组
1. 不同的区在磁盘上⼤概率是不连续的,那么这个问题其实是InnoDB如何⾼效的的管理区?
• 当表中的数据越来越多,为了有效的管理区,定义了区组的结构,每个区组固定管理256个区即
256MB ,通过区组可以在物理结构层⾯⾮常⾼效的管理和定位到每个区

• 第⼀个区组中的⾸个区的前四⻚⽐特殊,也就是初始⻚中的前4⻚,分别是:
◦ File Space Header: 表空间和区组中条⽬信息
◦ Insert Buffer Bitmap:Change Buffer相关信息
◦ File Segment inode: 段信息
◦ B-tree Node:索引根信息
◦ 其他为空闲⻚⽤来存储真实的数据
• 其他区组中⾸个区的结构都⼀样,前两个⻚分别是:
◦ Extent Descriptor(XDES):区组条⽬信息
◦ Insert Buffer Bitmap:Change Buffer相关信息
使⽤区组结构有效的管理区,每个区组固定管理256个区即 256MB ,区组条⽬信息中会记录每个
区的偏移并⽤双向链表连接。
8.以上这些数据结构还有优化空间吗
1. 以上讲到的区、区组还有⻚这种都是物理结构
2. 在物理结构的基础上,定义了⼀个逻辑上的概念,也就是"段";
• "段"并不对应表空间中的连续的物理区域,可以看做是 "区" 和 "⻚" 的⼀个附加标注信息,段的主
要作⽤是区分不同功能的区和在碎⽚区中的⻚,主要分为"叶⼦节点段"和"⾮叶⼦节点段"等,这两
个段和我们常说的B+树索引中的叶⼦、⾮叶⼦节点对应,可以简单的理解为"⾮叶⼦节点段" 存储
和管理索引树,"叶⼦节点段"存储和管理实际数据,从逻辑上讲,最终由 "叶⼦节点段" 和 "⾮叶⼦
节点段" 等段构成了表空间 .ibd ⽂件,如下图所⽰:

答:当然是有的,InnoDB使⽤"段"这个逻辑结构区分不同功能的区和在碎⽚区中的⻚,并按功能分 为"叶⼦节点段"和"⾮叶⼦节点段",做为B+树索引中的叶⼦、⾮叶⼦节点,从⽽进⼀步提升查询效 率。
衍生:
上⾯讲的所有操作是在哪⾥进⾏的?
• 所有的数据库操作都是在内存中进⾏的,最终会把修改结果刷回磁盘中对应的⻚中。
查询数据时MySQL会⼀次把表空间中的数据全部加载到内存吗?
• 当然不是,使⽤InnoDB存储引擎创建表,在查询数据时会根据表空间内部定义的数据结构(⼀般为索引),定位到⽬标数据⾏所在的⻚,只把符合查询要求的⻚加载到内存。
每查询⼀条数据都要进⾏⼀次磁盘I/O吗?
• 不⼀定,每次查询都会把磁盘中数据⾏对应的数据⻚加载到内存中,如果当前查询的数据⾏已经在内存中,则直接从内存中返回结果,从⽽提⾼查询效率。
3.页结构
⻚在MySQL运⾏的过程中起到了⾮常重要的作⽤,为了能发挥更好的性能,可以结合⾃⼰系统的
业务场景和数据⼤⼩,对⻚相关的系统变量进⾏调整,⻚的⼤⼩就是⼀个⾮常重要的调整项。

1.页都有哪些分类?需要重点了解那种?
• InnoDB在不同的使⽤场景定义多种不同类型的⻚,常⽤的有 数据⻚ 、 Undo Log⻚ 、
Change Buffer⻚ 、 Extent Descriptor(XDES)⻚ 、 InnoDB段信息⻚ 等,每种⻚的
数据结构都不相同,其中最需要我们关注的就是数据⻚,由于InnoDB中有个概念叫 "索引即数
据",所以也叫做索引⻚。
• 不论哪种类型的⻚都具有⻚头(File Header)和⻚尾(File Trailer)两个信息
2.页头和页尾都包含了哪些信息?

1. ⻚头 - File Header
• ⻚号: FIL_PAGE_OFFSET 占⽤ 4Byte ,相当于⻚的⾝份证号,通过这个⻓度可以计算出每个
InnoDB表中最多可以拥有 2^(4*8)-1约42亿 个⻚,表空间第⼀个⻚编号从0开始,之后的⻚号
分别是1,2,3…依此类推,具体⻚的偏移量计算公式为:⻚号 * 每⻚⼤⼩;那么按照每个⻚默认
16KB⼤⼩计算,⼀个表空间最⼤容量为 2^(4*8) * 16KB = 64TB ,这也是InnoDB表空间最
⼤容量是64T的原因;
• 上⼀⻚⻚号: FIL_PAGE_PREV
• 下⼀⻚⻚号: FIL_PAGE_NEXT 多个⻚通过这两个信息组成双向链表,即使不同的⻚地址不连
续,也可以通过链表连接
• 表空间ID: FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID ,当前⻚属于哪个表空间
• ⻚类型: FIL_PAGE_TYPE ,数据⻚对应的⻚类型是 FIL_PAGE_INDEX = 0x45BF
• 最近⼀次修改的LSN: FIL_PAGE_LSN ,占⽤8Byte
• 已被刷到磁盘的LSN: FIL_PAGE_FILE_FLUSH_LSN ,占⽤8Byte
• 校验和: FIL_PAGE_SPACE_OR_CHKSUM ,⽤于⻚的完整性校验
2. ⻚尾 - File Trailer
• 最近⼀次修改的LSN
• 校验和:对应⻚头中的校验和如果在数据传输的过程中数据丢失或异常中断,导致⼀个数据⻚不完整就可以通过⻚头和⻚尾的校验和进⾏验证,验证算法默认使⽤ CRC32

什么是LSN?
LSN:是"Log Sequence Number"的缩写,表⽰⽇志序号。⽤⼀个任意的、不断增加的值表⽰⽇志中 记录的操作对应的时间点,⽤8字节的⽆符号⻓整形表⽰,后⾯会详细介绍如何⽣成LSN的值
除了⻚头和⻚尾,数据⻚中还有哪些信息?
• ⻚头和⻚尾中的各个字段描述了当前⻚的类型以及在⽂件系统中的位置,也就是说通过⻚头可以找到对应的⻚。数据⻚的主要功能是保存数据,在⼀个数据⻚中,除了⻚头与⻚尾占⽤的46个字节之外的空间都⽤来存储真正的数据,也就是数据⾏,数据⾏会与表⾥的数据⾏⼀⼀对应,基于这⼀特性MySQL也被称为 "⾏式数据库" ,也可以把除了⻚头⻚尾的区域称为⻚主体。
⻚主体中包含哪些信息?
• ⻚主体中的信息都是和数据相关的,其中包括刚才提到了数据⾏,还有为了提⾼查询效率的⻚⽬
录 Page Directory 和为了⽅便操作和管理数据⻚的数据⻚头 Page Header ,这⼜是三个⾮
常重要的概念,接下来我们逐个讨论
3.数据行由哪些信息组成
数据⾏主要存储真实数据,为了⽅便数据的管理与描述,InnoDB在每个数据⾏中还添加了⼀些额
外(管理)信息,于是每⼀个 DYNAMIC 数据⾏都可以划分为两部分,⼀部分存储额外信息,⼀部分
存储真实数据,额外信息部分包含变⻓字段⻓度列表和NULL值列表两个⼤⼩不确定的区域,以及
固定占5字节及40BIT的头信息区域,头信息中存储了⾏的基本信息,包括⾏在⻚内的位置
heap_no 、⾏类型 record_type 、下⼀⾏的地址偏移量 next_record 等6项信息,如下图
所⽰:

• 数据⾏可以划分为两部分,⼀部分存储额外信息,⼀部分存储真实数据
• 额外信息部分包含变⻓字段⻓度列表和NULL值列表两个⼤⼩不确定的区域,以及固定占5字节的头信息区域
4.数据行是怎么组织在一起的?
• 数据⾏通过下⼀⾏的地址偏移量,即 next_record 将⻚内所有数据⾏组成了⼀个单向链表,这
⾥要注意的是,地址偏移量指向的是下⼀⾏中真实数据的起始地址,这样做的好处是,向右是真实
数据,向左就是头信息,⽽⽆需额外的⻓度计算,如图所⽰:

5.怎么识别新页中的第一行和最后一行
了解了⾏的基本结构和组织⽅式之后,那么当遍历⻚中的⾏时,从哪⾥开始到哪⾥结束呢?为了解
决这个问题,每当创建⼀个新⻚,都会⾃动分配两个⾏,⼀个是⾏类型为2的最⼩⾏ Infimun ,
heap_no 位置固定为0号,和⼀个是⾏类型为3的最⼤⾏ Supremun , heap_no 位置固定为1
号,这两个⾏并不存储任何真实信息,⽽是做为数据⾏链表的头和尾,虽然不存储真实数据,但它
们的数据结构和真实数据⾏完全⼀致,只不过数据区域存储的是代表它们⾝份的固定字符串
Infimun 和 Supremun ,新⻚中没有数据时,最⼩⾏ Infimun 的 next_record 直接连接
最⼤⾏ Supremun ,最⼤⾏不连接任何⾏,它的 next_record 为0;

6.当向⼀个新⻚插⼊数据时是如何执⾏的?
当向⼀个新⻚插⼊数据时, heap_no 会从 2 号开始递增,表⽰当前记录在⻚⾯堆中的相对位
置;如果是真实数据则 record_type 为0,如果是索引⽬录(B+树⾮叶节点)数据则
record_type 为1;再将 Infimun 连接第⼀个数据⾏,最后⼀⾏真实数据⾏连接
Supremun ,这样数据⾏就构建成了⼀个单向链表,更多的⾏数据插⼊后,会按照主键从⼩到⼤
的顺序进⾏链接;为了使⻚的结构更加清晰,通常将⻚中有数据⾏的区域称为⽤⼾数据区 User
Records ,把未被数据⾏占⽤的区域称为空闲区 Free Space ,如下图所⽰:

7.如果要查询的数据在某⼀个⻚中,如何定位它在⻚中的位置,⼀条条遍历吗?
⼀条条遍历的查询效率⾼不⾼?
• 从头开始遍历是⼀个最简单的⽅法,也可以实现数据的查找,当按主键或索引查找某条数据时,从头⾏ infimun 开始,沿着链表顺序逐个⽐对查找,但⼀个⻚有16KB,通常会存在数百⾏数据,
每次都要遍历数百⾏,⽆法满⾜⾼效查询。
如何提高页内的查询效率?页目录
• 为了提⾼查询效率,InnoDB采⽤⼆分查找来解决查询效率问题。
• 具体实现⽅式是,在每⼀个⻚中加⼊⼀个叫做⻚⽬录 Page Directory 的结构,将⻚内包括头
⾏、尾⾏在内的所有⾏进⾏分组,约定头⾏单独为⼀组,其他每个组最多8条数据,同时把每个组
最后⼀⾏在⻚中的地址,按主键从⼩到⼤的顺序记录在⻚⽬录中在,⻚⽬录中的每⼀个位置称为⼀
个槽,每个槽都对应了⼀个分组,这样在插⼊数据⾏完成链接后,⼀旦最后⼀个分组中的数据⾏超
过分组的上限8个时,就会分裂出⼀个新的分组,为了快速判断每个分组是否达到了8个的上限,在
每个分组最后⼀⾏中⽤ n_owned 记录了这个分组内的⾏数,与此同时在⻚⽬录中创建⼀个新的
槽,后续插⼊的⾏都遵守这个规则;
• 后续在查询某⾏时,就可以通过⼆分查找,先找到对应的槽,然后在槽内最多8个数据⾏中进⾏遍历即可,从⽽⼤幅提⾼了查询效率;
• 例如要查找主键为6的⾏,先⽐对槽中记录的主键值,定位到最后⼀个槽2,再从最后⼀个槽中的第⼀条记录遍历,第⼆条记录就是我们要查询的⽬标⾏。

8.关于事务,索引这些信息在页中怎么记录

下图是数据⻚的完整结构,以及所占的磁盘空间

4.行结构
• 真实的数据在表空间以数据⾏的形式存储,也就是说每⼀条数据都对应着表中的⼀⾏,数据⾏在⻚中的位置如下图所⽰:


1.InnoDB⽀持的数据⾏格式都有哪些?
• InnoDB⽀持四种⾏格式,分别是: REDUNDANT 冗余格式, COMPACT 紧凑格式, DYNAMIC 动态格式和 COMPRESSED 压缩格式,默认是 DYNAMIC 格式
• 可以使⽤以下SQL查看⾏格式:
# 查看系统变量中设置的⾏格式
mysql> SHOW VARIABLES LIKE 'innodb_default_row_format';
+---------------------------+---------+
| Variable_name | Value |
+---------------------------+---------+
| innodb_default_row_format | dynamic | # 当前使⽤的⾏格式
+---------------------------+---------+
1 row in set, 1 warning (0.02 sec)# 使⽤SHOW table STATUS查看数据库中的所有表
mysql> SHOW TABLE STATUS IN test_db\G
# ... 省略
*************************** 6. row ***************************Name: studentEngine: InnoDBVersion: 10Row_format: Dynamic # 指定数据库使⽤的⾏格式Rows: 5Avg_row_length: 3276Data_length: 16384
Max_data_length: 0
Index_length: 0Data_free: 0Auto_increment: NULLCreate_time: 2023-09-19 15:27:27Update_time: NULLCheck_time: NULLCollation: utf8mb4_general_ciChecksum: NULLCreate_options: row_format=DYNAMICComment:
# ... 省略
8 rows in set (0.04 sec)# 通过查询INFORMATION_SCHEMA.INNODB_TABLES表查看指定表的⾏格式
mysql> SELECT NAME, ROW_FORMAT FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE
NAME='test_db/student';
+-----------------+------------+
| NAME | ROW_FORMAT |
+-----------------+------------+
| test_db/student | Dynamic | # 指定表使⽤的⾏格式
+-----------------+------------+
1 row in set (0.00 sec)如何指定行格式?
• 可以通过全局变量设置⾏格式,也可以在创建表中通过 ROW_FORMAT ⼦句指定⾏格式:
# 通过全局变量设置
SET GLOBAL innodb_default_row_format=DYNAMIC;# 在创建表时明确的指定⾏格式
CREATE TABLE t1 (c1 INT) ROW_FORMAT=DYNAMIC;⼀个 DYNAMIC 格式的数据⾏会被分为两部分,⼀个部是存储真实数据的区域,⼀部分是存储额外信息区域。
2.数据区是怎么存储真实数据的?
• 数据区在数据⾏中的位置如下图所⽰:

• 从分隔线向右第⼀个字段存储真实数据的主键值,对于主键值有以下⼏种情况:
◦ 如果表中定义了主键,则直接存储主键的值;
◦ 如果是复合主键会根据列定义的顺序依次排列在这⾥;
◦ 如果没有主键,会优先使⽤第⼀个不允许为NULL的 UNIQUE 唯⼀列作为主键;
◦ 如果既没有主键也没有唯⼀键,那么InnoDB会构建⼀个6字节的字段 DB_ROW_ID 作为⾏的唯
⼀标识,存储在真实数据的头部
• 紧接着是在事务运⾏中两个⾮常重要的固定字段
◦ 6字节的事务ID字段 DB_TX_ID ,记录创建或最后⼀次修改该记录的事务ID
◦ 7字节的回滚指针字段 DB_ROLL_PTR ,如果在事务中这条记录被修改,指向这条记录的上⼀
个版本
• 接下来就是除了主键和值为NULL的列之外,其他列的真实数据,按照顺序从左到右依次排列
• ⾄于为什么不存储NULL值,原因很简单,就是为了节少空间,所有允许为NULL的列都会在⾏额外信息区的NULL值列表中进⾏标识,后⾯我们会详细详解,以上就是数据⾏对真实数据的存储⽅
式。

3.额外(管理)信息区包含了关于行的哪些信息?

4.头信息区域包含了哪些信息?

• 下⼀⾏地址偏移量: next_record 占16bit,通过这个信息将所有的⾏链接成⼀个单向链表
• ⾏类型: record_type 占3bit,包括四种类型:
◦ 0:普通数据⾏
◦ 1:索引⽬录⾏
◦ 2:⻚内最⼩⾏infimun
◦ 3:⻚内最⼤⾏supremun
• ⾏在整个⻚中的位置: heap_no 占13bit;
• 分组的⾏数: n_owned 占4bit,只在该⾏是分组最后⼀⾏才有值,这样就可以快速查询⾏数,⽽
不⽤⼀条条的累加了
• B+树索引树每层最⼩值标记: min_rec_flag 占1bit,如果当前⾏的类型是⽬录⾏也就是
record_type=1 ,同时也是B+索引树某层的最⼩值,则会置为1,会在索引查询时⽤到,后⾯
我们讲索引时再介绍
• 删除标记: delete_mask 占 1bit ,从⻚中删除数据⾏时,并不会直接移除,⽽是修改这个删
除标记为 1
• 预留区:占2bit
5.删除一行记录时在InnoDB内部执行了哪些操作?
从⻚中删除数据⾏时,并不会直接移除,⽽是修改 delete_mask 这个删除标记为 1 ,并将
next_record 改为 0 ,同时将上⼀⾏的 next_record 指向后续的⾏,从⽽把该⾏从链表中
断开,如果执⾏事务提交后,则将这⾏的 next_record 指向⼀个被称为垃圾链表的区域,这个
链表会被⽤在事务回滚中。

6.NULL列表有啥用?列表中的值是什么?
• 头信息区再向右就是NULL值列表的可变区域,⽤来存储数据⾏中所有列允许为Null的值从⽽节省
空间,具体的实现⽅式是,⽤1BIT的⼤⼩来表⽰⾏中某⼀列是否为空,这样空列就不需要记录在真
实数据区域中了
• 为每个没有定义 NOT NULL 约束也就是可以为NULL的列在NULL值列表中都安排了⼀个bit位,按列序号从⼩到⼤的顺序从右⾄左依序安排,这就是常说的逆序排列,NULL值列表最⼩1字节即
8bit,如果没有那么多可以为NULL的列,则会⽤0补满8bit,如果为值为NULL的列超过8个,则新
开辟1字节的空间,依此类推;
• 如果某列为空,则NULL值列表中对应的bit设置为1,这样只⽤了⼀bit就存储了NULL列,⾮常节省空间

7.变长字段列表有啥用?列表中的值是什么?
• 查看编码集所占的字节数
mysql> show charset;
+----------+---------------------------------+---------------------+--------+
| Charset | Description | Default collation | Maxlen |
+----------+---------------------------------+---------------------+--------+
| armscii8 | ARMSCII-8 Armenian | armscii8_general_ci | 1 |
| ascii | US ASCII | ascii_general_ci | 1 |
| big5 | Big5 Traditional Chinese | big5_chinese_ci | 2 |
| binary | Binary pseudo charset | binary | 1 |
| euckr | EUC-KR Korean | euckr_korean_ci | 2 |
| gb18030 | China National Standard GB18030 | gb18030_chinese_ci | 4 |
| gb2312 | GB2312 Simplified Chinese | gb2312_chinese_ci | 2 |
| gbk | GBK Simplified Chinese | gbk_chinese_ci | 2 |
# ... 省略
| utf32 | UTF-32 Unicode | utf32_general_ci | 4 |
| utf8mb3 | UTF-8 Unicode | utf8mb3_general_ci | 3 |
| utf8mb4 | UTF-8 Unicode | utf8mb4_0900_ai_ci | 4 |
+----------+---------------------------------+---------------------+--------+

• ⾏结构的最左侧是变⻓字段列表,也叫可变字段⻓度列表,在这个列表中记录了数据⾏中所有变⻓字段的实际⻓度,这样做的⽬的,是为了在真实数据区域,可以根据列的⻓度进⾏列与列之间的分割;
• 需要记录的变⻓字段类型常⻅的有varchar、varbinary、text、blob,以及当使⽤了例如utf-8、
gbk等变⻓字符集的char类型,当char类型的字节数可能超过768个字节时,⽐如使⽤utf8mb4字
符集时定义了char(255),这个字段的最⼤字节数是4*255=1020
• 每个变⻓字段分配1 ~ 2个字节来存放这些字段的真实⼤⼩,放置顺序也是按表中字段的顺序从右⾄左逆序排列;
create table test_varchar ( //自增属性'id' bigint not null auto_increment,'name' varchar(20000) null,primary key('id')
);# 返回错误
ERROR 1074 (42000): Column length too big for column 'name' (max = 16383); use
BLOB or TEXT instead• 2个字节最⼤可以表⽰65535个字节,按照最⼤⻓度字符串,⽐如 utf8mb4,⼀个字符占⽤最多4个字节计算,2个字节最多可以表⽰65535/4=16383个字符,列数据类型varchar的⻓度上限16383就是根据这个计算来的;
• 需要特别说明的是,如果text、blob存储的内容过⼤,⼀个⻚已经不够放了,就会把这个列放⼊⼀个叫"溢出⻚"的独⽴空间中,在这个数据⾏对应的真实数据处,只使⽤20个字节来标记这个溢出⻚的位置信息

衍生:
如何记录变长字段的实际长度
• 不同的字符集在处理字符对应的最⼤字节⻓度不同,以如 ascii 最⼤1个字节, utf8mb3 最⼤3
个字节, utf8mb4 最⼤4个字节,如下所⽰
| ascii | US ASCII | ascii_general_ci | 1 |
| utf8mb3 | UTF-8 Unicode | utf8mb3_general_ci | 3 |
| utf8mb4 | UTF-8 Unicode | utf8mb4_0900_ai_ci | 4 |• 当使⽤varchar( M )指定⼀个字段的最⼤字符数时,该字段真实使⽤的字节数与建表时指定的字符
集有关,如果指定的字符集单个字符最⼤占 W 个字节,从理论上讲,该列最多使⽤的字节数 M *
W ,如果 M * W <= 255 则⽤⼀个字节记录这个变⻓字段的⻓度就⾜够了
• 如果 M * W > 255 可能分为两种情况,假设当前变⻓字段实现占⽤了 L 个字节:
◦ L <= 127 ⽤⼀个字节表⽰⻓度
◦ L > 127 ⽤两个字节表⽰⻓度
读取长度时如何处理粘包问题?
• 也就是说在读取变⻓字段⻓度时,如何确定读取⼀个字节还是两个字节?
• 在任何时候都是先读⼀个字节,然后判断这个字节的⾼位是否为0,如果是0则表⽰当前⽤⼀个字节表⽰⻓度,如果是1则表⽰当前⽤两个字节表⽰⻓度
• 为1时再读⼀个字节,然后合并在⼀起进⾏解析得到该字段真实的使⽤的字节数,⽽且第⼆个BIT位表⽰是否使⽤溢出⻚
默认数据⻚⼤⼩为16KB,数据⻚中⼀个数据⾏的⼤⼩最⼤为8KB

8.其他的行格式与DYNAMIC有什么区别?
REDUNDANT 冗余格式
已被淘汰,之所以存在是为了与旧版本 MySQL 兼容,不建议使⽤,这⾥不再讨论。
COMPRESSED 压缩格式
⾏结构与 DYNAMIC 完全相同,只是会对数据进⾏压缩,以减少对空间的占⽤。
COMPACT 紧凑格式
在结构上与 DYNAMIC 相同,只是对超⻓字段的处理上有些区别,它不会把所有超⻓数据都放在溢出⻚中,⽽是会在本⾏中保留前768个字节的数据,多出的部分放在溢出⻚中,溢出⻚的地址额外⽤20个字节表⽰,那么在本⾏的列中就会占⽤768+20个字节。
小结:

