MySQL——学习InnoDB(1)
MySQL——学习InnoDB(1)
文章目录
- MySQL——学习InnoDB(1)
- 1. InnoDB的前世今生
- 1.1 诞生发展
- 1.2 核心设计
- 1.3 关键进化
- 2. InnoDB和MyISAM在性能和安全上的区别
- 2.1 性能对比
- 2.2 安全对比
- 3. InnoDB架构图(内存结构+磁盘结构)
- 3.1 为什么要设计成内存结构和磁盘结构两个部分?
- 4. InnoDB的磁盘结构
- 4.1 表空间是什么
- 4.1.1 表空间和表空间文件的关系
- 4.2 用户数据在表空间中如何存储
- 4.2.1 小区例子
- 4.3 为什么要使用页这个管理单元
- 4.3.1 为什么默认是16KB(接水例子)
- 4.3.2 页默认16KB不会产生浪费吗
- 4.4 数据页的基本特性
- 4.4.1 页的物理属性
- 4.4.2 页的逻辑结构
- 4.4.3 核心组成部分详解
- 1. File Header(文件头)
- 2. Page Header(页头)
- 3. Infimum + Supremum Records
- 4. User Records(用户记录)
- 5. Page Directory(页目录)
- 6. File Trailer(文件尾)
- 4.4.4 页的运行时行为
- 1. 记录插入流程
- 2. 空间回收
- 3. 页分裂阈值
- 4.4.5 页类型扩展
- 4.4.6 性能优化建议
- 4.5 大范围查询优化:区(Extent)与顺序I/O
- 4.5.1 核心问题:随机I/O的性能瓶颈
- 4.5.2 InnoDB的解决方案:区(Extent)
- 1. 区的定义
- 2. 区如何提升性能
- 4.5.3 关键优化机制
- 1. 预读(Read-Ahead)
- 2. 区组(Extent Group)
- 4.5.4 区如何管理页
- 4.6 段的作用
- 4.6.1 核心定位
- 4.6.2 分类与功能
- 4.6.3 段的空间管理机制
- 1. 分配策略对比
- 2. 碎片区(Fragmented Extent)的特殊性
- 3. 段与区的映射关系
- 5. 页的结构
- 5.1 页大小设置
- 5.2 页的分类
- 5.3 页头/页尾结构
- 5.3.1 ⻚头 - File Header
- 5.3.2 ⻚尾 - File Trailer
- 5.4 页主体结构
- 5.4.1 数据行格式(重点)
- 1. 变长字段长度列表
- 2. NULL标记位
- 3. 头信息(40BIT)
- 4. 主键值、事务ID、回滚指针
- 5.4.2 首尾记录标识
- 5.4.3 页目录查询
- 5.5 事务信息存储
- 6. 行结构
- 6.1 行格式对比
- 6.1.1 查看行格式
- 6.1.2 指定行格式
- 6.1.3 DYNAMIC行特性
- 6.1.4 变长字段存储
- 6.1.5 溢出指针结构
- 6.1.6 格式差异对比
- 附录:InnoDB存储全景图
1. InnoDB的前世今生
InnoDB做为MySQL的默认存储引擎,其实并不是MySQL开发的,⽽是由 Innobase Oy 公司所开发,2006年五⽉时由甲⻣⽂公司并购。这也是MySQL做为⼀个开源数据库的好处之⼀,第三⽅开发者可以根据⾃⼰的业务场景开发出⾼性能的存储引擎,像 Innobase Oy 公司开发的InnoDB,⼀旦被官⽅采纳,那么就可能通过被收购从⽽得到可观的收⼊,官⽅的产品也得到了有效的扩展,是⼀个双赢的结果。
1.1 诞生发展
- 2001年:Heikki Tuuri开发,首个支持ACID的MySQL引擎
- 2005年:被Oracle收购
- 2010年:成为MySQL 5.5默认引擎
- 2018年:MySQL 8.0实现原子DDL
1.2 核心设计
- 事务引擎:
- MVCC多版本控制
- 行锁+间隙锁
- Redo/Undo日志
- 存储结构:
- 聚簇索引组织
- 16KB页存储
- B+树索引
1.3 关键进化
版本 | 重大改进 |
---|---|
5.5 | 默认引擎,缓冲池优化 |
5.7 | 全文索引,GIS支持 |
8.0 | 原子DDL,哈希索引 |
2. InnoDB和MyISAM在性能和安全上的区别
2.1 性能对比
维度 | InnoDB | MyISAM |
---|---|---|
读写速度 | 写入稍慢(需维护事务日志) | 读操作更快(无事务开销) |
并发能力 | 高并发优(行级锁+MVCC) | 低并发(表锁,读写阻塞) |
索引效率 | 聚簇索引,主键查询极快 | 非聚簇索引,索引/数据分离 |
缓存机制 | 缓存数据和索引(Buffer Pool) | 仅缓存索引(Key Buffer) |
全表扫描 | 较慢(需处理事务结构) | 更快(简单数据存储格式) |
2.2 安全对比
维度 | InnoDB | MyISAM |
---|---|---|
崩溃恢复 | ✅ 自动恢复(Redo Log) | ❌ 需手动修复(REPAIR TABLE ) |
事务支持 | ✅ 完整ACID | ❌ 不支持 |
外键约束 | ✅ 支持 | ❌ 不支持 |
数据一致性 | ✅ 行级锁+MVCC | ❌ 表锁易丢失更新 |
日志保护 | ✅ Redo/Undo Log | ❌ 仅错误日志 |
3. InnoDB架构图(内存结构+磁盘结构)
内存结构:
- Buffer Poll
- Change Buffer
- Log Buffer
- 自适应哈希
磁盘结构:
- 系统表空间
- 独立表空间
- 通用表空间
- 临时表空间
- 撤销表空间
- Undo Log
- 双写缓冲区
3.1 为什么要设计成内存结构和磁盘结构两个部分?
我们从MySQL实现的⻆度来思考这个问题,数据库的作⽤就是保存数据,⽤⼾的真实数据最终都会保存在磁盘上,在查询数据的过程中,如果每次都从磁盘上读取会严重影响效率,为了提⾼数据的访问效率,InnoDB会把查询到的数据缓存到内存中,当再次查询时,如果⽬标数据已经存在于内存中,就可以从内存中直接读取,从⽽⼤幅提升效率。也就是说磁盘结构中的⽂件是⽤来保存数据实现数据持久化的,内存结构是⽤来缓存数据提升效率的。
4. InnoDB的磁盘结构
4.1 表空间是什么
表空间⽂件是⽤来存储表中数据的⽂件,表空间⽂件的⼤⼩由存储的数据多少决定,不同的表空间⽂件存储数据的种类也有所不同,在MySQL中表空间分为五类,包括:系统表空间、独⽴表空间、通⽤表空间、临时表空间和撤销表空间,这些在上⾯的InnoDB架构图中都有体现。
当使⽤InnoDB存储引擎创建⼀个表时,默认会在数据⽬录对应的数据库⼦⽬录中⽣成相应的表空间⽂件,以 .ibd 为⽂件的后缀,⽤来存储数据和索引,如果每个表都对应⼀个表空间⽂件,称为独⽴表空间,在MySQL5.7及以后的版本中默认为每个表⽣成独⽴表空间,可以通过系统变量innodb_file_per_table[=ON|OFF] 进⾏控制,如果关闭这个选项,则所有表的数据都在系统表空间中存储,独⽴表空间⽂件如下所⽰:
root@turuichen-VMware-Virtual-Platform:/var/lib/mysql/oj# ll # 列出指定数据库⽬录下的所有⽂件
total 764
drwxr-x— 2 mysql mysql 4096 3月 28 17:58 ./
drwx------ 11 mysql mysql 4096 4月 14 17:49 …/
-rw-r----- 1 mysql mysql 131072 4月 6 00:06 oj_questions.ibd # .ibd⽂件是使⽤InnoDB存储引擎创建的表对应的表空间⽂件
-rw-r----- 1 mysql mysql 147456 3月 28 19:50 question_list_info.ibd
-rw-r----- 1 mysql mysql 180224 3月 29 00:14 question_lists.ibd
-rw-r----- 1 mysql mysql 163840 4月 11 00:28 submit_records.ibd
-rw-r----- 1 mysql mysql 147456 3月 27 08:57 users.ibd
4.1.1 表空间和表空间文件的关系
表空间可以理解为MYSQL为了管理数据⽽设计的⼀种数据结构,主要描述的对结构的定义,表空间⽂件是对定义的具体实现,以⽂件的形式存在于磁盘上。
4.2 用户数据在表空间中如何存储
-
⾸先明确⼀点,⽤⼾的数据以数据⾏的⽅式存储在对应的表空间⽂件中,那么表空间中很多个数据⾏就需要进⾏管理,以便后续进⾏⾼效的查询;
-
为了⽅便管理,表空间由段 (segment)、区组(group)、区 (extent)、⻚ (page) 、数据⾏组成,其中⻚是 InnoDB 磁盘管理的最⼩单位;
可以这么理解,若⼲数据⾏组成了⻚,多个⻚组成了区,多区组成了区组,多个区组组成了段,多个段组成了表空间。
4.2.1 小区例子
在小区中,每一个房间号就是页号,房间就是一个个页,而一个单元楼就是由多个房间组成(页),对应区,而多个连续的单元楼又组成一栋楼,对应区组,所有栋楼组成一个小区,小区就是一个段,只是有的小区是公寓形式的小区,有的小区是居民楼,有的是别墅区,根据存储的数据类别进行区别。
4.3 为什么要使用页这个管理单元
⾸先要明确⼀点,MySQL中的⻚是应⽤层的⼀个概念,是MySQL根据⾃⾝的应⽤场景,定义的⼀种数据结构。
通常操作系统中的⽂件系统在管理磁盘⽂件时以4KB⼤⼩为⼀个管理单元,称为"数据块",但是在数据库的应⽤场景⾥,查询时数据量都⽐较⼤,如果也使⽤4KB做数据存储的最⼩的单元,就显的有点⼩了,同时会造成频繁的磁盘I/O,导致降低效率;
所以MySQL根据⾃⾝情况定义了⼤⼩为16KB的⻚,做为磁盘管理的最⼩单位;
每次内存与磁盘的交互⾄少读取⼀⻚,所以在磁盘中每个⻚内部的地址都是连续的,之所以这样做,是因为在使⽤数据的过程中,根据局部性原理,将来要使⽤的数据⼤概率与当前访问的数据在空间上是临近的,所以⼀次从磁盘中读取⼀⻚的数据放⼊内存中,当下次查询的数据还在这个⻚中时就可以从内存中直接读取,从⽽减少磁盘I/O,提⾼性能。
4.3.1 为什么默认是16KB(接水例子)
当我们在接水时,常常是使用容器来装水,每次装满然后放到水缸或一个大容器中。水管连接的水库就像是磁盘,我们每次通过水管接水就好比读写一次磁盘,每次接完水,又要放到水缸中。水缸好比一块内存区域,而如果我的接水容器很小,那么每接一次固定体积的水,需要的接水次数要比使用更大接水容器的接水次数多,导致效率更低;但如果我的接水容器过大,而我不需要那么多的水,也就是,导致每次会接出额外的水,也会导致内存浪费和效率低。因此,选定一个适当的容器大小(页大小)是非常重要的,经过计算,选定16KB作为MySQL的默认页大小。
4.3.2 页默认16KB不会产生浪费吗
InnoDB 默认使用 16KB 的页大小(可通过 innodb_page_size
调整),看似可能浪费空间,但实际上通过 局部性原理(Locality Principle) 实现了空间与性能的最佳平衡。
局部性原理的核心思想
- 时间局部性(Temporal Locality):
如果一个数据被访问,那么它很可能在短时间内再次被访问(比如事务频繁修改同一条记录)。 - 空间局部性(Spatial Locality):
如果一个数据被访问,那么它附近的数据也可能很快被访问(比如范围查询、索引遍历)。
4.4 数据页的基本特性
InnoDB 的数据页(Page)是存储和管理的最小物理单元,其设计直接影响数据库的性能和可靠性。以下是核心特性详解:
4.4.1 页的物理属性
特性 | 说明 |
---|---|
固定大小 | 默认 16KB(可通过 innodb_page_size 调整为 4K/8K/16K/32K/64K) |
不可分割 | 即使只存1字节数据,也会占用完整16KB空间 |
唯一标识 | 通过 (space_id, page_no) 全局定位(如 (1, 123) 表示表空间1的第123页) |
修改页大小(需初始化前配置):
# my.cnf
[mysqld]
innodb_page_size=32K # 重启后不可更改
4.4.2 页的逻辑结构
┌───────────────────────────────────────┐
│ InnoDB Page (16KB) │
├───────────────────┬──────────────────┤
│ File Header (38字节) │
├───────────────────┼──────────────────┤
│ Page Header (56字节) │
├───────────────────┼──────────────────┤
│ Infimum + Supremum Records │
├───────────────────┼──────────────────┤
│ User Records (实际数据行) │
├───────────────────┼──────────────────┤
│ Free Space (未分配空间) │
├───────────────────┼──────────────────┤
│ Page Directory (页目录) │
├───────────────────┼──────────────────┤
│ File Trailer (8字节) │
└───────────────────┴──────────────────┘
4.4.3 核心组成部分详解
1. File Header(文件头)
字段 | 大小 | 作用 |
---|---|---|
FIL_PAGE_SPACE_ID | 4字节 | 表空间ID |
FIL_PAGE_OFFSET | 4字节 | 页号(在表空间内的物理偏移) |
FIL_PAGE_TYPE | 2字节 | 页类型(0x45BF=数据页,0x0002=索引页等) |
FIL_PAGE_LSN | 8字节 | 最后修改的日志序列号(用于崩溃恢复) |
2. Page Header(页头)
字段 | 说明 |
---|---|
PAGE_N_DIR_SLOTS | 页目录中的槽位数(用于二分查找) |
PAGE_HEAP_TOP | 空闲空间起始位置(动态增长方向) |
PAGE_N_HEAP | 页内记录数(包括Infimum/Supremum伪记录) |
PAGE_LAST_INSERT | 最后插入记录的位置 |
3. Infimum + Supremum Records
- 伪记录:每个页固定有这两条系统记录
Infimum
:比任何主键都小的虚拟记录(标识页内最小值)Supremum
:比任何主键都大的虚拟记录(标识页内最大值)
- 作用:作为B+树叶子节点的边界标记
4. User Records(用户记录)
-
存储格式:
| 变长字段长度列表 | NULL标志位 | 记录头 | 主键值 | 事务ID | 回滚指针 | 列数据... |
-
记录头结构:
struct rec_header { uint8_t deleted_flag; // 删除标记 uint8_t min_rec_flag; // 非叶子节点最小记录标记 uint8_t n_owned; // 该记录拥有的记录数(页目录用) uint16_t heap_no; // 记录在堆中的序号 uint8_t record_type; // 记录类型(0=普通,1=非叶子节点等) uint16_t next_record; // 下一条记录的相对位置 };
5. Page Directory(页目录)
-
作用:加速页内记录查找(类似索引的索引)
-
实现原理:
- 将记录分组(每组4-8条),记录每组最后一条记录的地址到槽位(Slot)
- 查找时先二分定位槽位,再在组内线性搜索
槽位0 → 记录A (heap_no=2) 槽位1 → 记录D (heap_no=5) 槽位2 → 记录G (heap_no=8)
6. File Trailer(文件尾)
- 8字节校验和:用于检测页是否完整写入磁盘(防止部分写问题)
4.4.4 页的运行时行为
1. 记录插入流程
2. 空间回收
- 删除记录:仅标记
deleted_flag
,空间不立即回收 - 空间重用:新插入记录优先使用已删除的空间(通过
PAGE_FREE
链表管理)
3. 页分裂阈值
-
触发条件:剩余空间不足容纳新记录时
-- 查看页填充状态 SHOW ENGINE INNODB STATUS\G -- 在"BUFFER POOL AND MEMORY"段查找"Page size"
4.4.5 页类型扩展
页类型 | 标识值 | 用途 |
---|---|---|
数据页 | 0x45BF | 存储表数据 |
索引页 | 0x0002 | 非叶子节点的索引记录 |
Undo日志页 | 0x0002 | 存储事务回滚日志 |
系统页 | 0x0003 | 数据字典信息 |
BLOB页 | 0x0004 | 存储溢出的大对象数据 |
4.4.6 性能优化建议
-
避免行溢出:
- 单行数据尽量不超过页大小的一半(约8KB)
-- 检查大记录 SELECT avg_row_length FROM information_schema.tables WHERE table_name='your_table';
-
监控页分裂:
-- 通过innodb_metrics监控 SELECT name, count FROM information_schema.INNODB_METRICS WHERE name LIKE '%page_split%';
-
优化主键设计:
- 自增主键可减少页分裂(顺序插入)
- 避免随机主键(如UUID)导致频繁分裂
4.5 大范围查询优化:区(Extent)与顺序I/O
当查询涉及跨越多页的数据时,InnoDB 通过 区(Extent) 和 预读机制 解决随机 I/O 性能问题。以下是深度解析:
4.5.1 核心问题:随机I/O的性能瓶颈
-
场景示例:
-- 查询跨越多个页的数据 SELECT * FROM orders WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31';
-
性能陷阱:
- 若数据分散在不同页(物理不连续),需多次磁盘寻址 → 高延迟
- 传统机械硬盘随机I/O性能可能比顺序I/O 低100倍
4.5.2 InnoDB的解决方案:区(Extent)
1. 区的定义
概念 | 说明 |
---|---|
物理组成 | 连续64个页(默认16KB/页 × 64 = 1MB) |
分配策略 | - 小表:使用碎片区中的零散页 - 大表(>32页):整区分配 |
管理单元 | 通过 XDES页(Extent Descriptor) 跟踪区的使用状态 |
新表默认7个初始页,放入碎片区,达到32个页的时候,后续每次都会申请一个完整的区。
2. 区如何提升性能
-
顺序I/O优化:
4.5.3 关键优化机制
1. 预读(Read-Ahead)
-
线性预读:
- 当顺序访问一个区中超过
innodb_read_ahead_threshold
(默认56)页时,异步预读下一个区
SHOW VARIABLES LIKE 'innodb_read_ahead_threshold';
- 当顺序访问一个区中超过
-
随机预读:
- 当检测到同一个区中的随机访问热点时,预读剩余页(默认关闭,因SSD普及)SET GLOBAL
SET GLOBAL innodb_random_read_ahead=ON; -- 谨慎使用
2. 区组(Extent Group)
- 物理连续:256个区(256MB)为一组,进一步提升顺序访问效率
- 分配策略:
新建表 → 从空闲区组分配
大表扩容 → 优先分配相邻区组
4.5.4 区如何管理页
区描述符(XDES)
struct xdes_entry {
uint32_t state; // 区状态(FREE/FRAG/FULL)
uint64_t bitmap; // 页使用位图(64位对应64个页)
list_node_t list; // 所属链表
};
4.6 段的作用
4.6.1 核心定位
InnoDB 的 段(Segment) 是比区(Extent)更高层级的逻辑管理单元,用于区分不同功能的数据存储区域,主要解决两类问题:
- 空间隔离:避免索引节点与数据页混用导致碎片化
- 性能优化:针对B+树不同部位(叶子/非叶子)采用差异化管理
4.6.2 分类与功能
段类型 | 存储内容 | 管理策略 | 典型场景 |
---|---|---|---|
叶子节点段 | 存储实际行记录 | 优先分配完整区(1MB) | 主键索引的叶子层 |
非叶子节点段 | 存储B+树索引目录 | 允许使用碎片区中的零散页 | 索引的根节点和中间层 |
回滚段 | Undo日志记录 | 固定大小的独立区组 | 事务回滚和MVCC |
4.6.3 段的空间管理机制
1. 分配策略对比
2. 碎片区(Fragmented Extent)的特殊性
-
共用池:所有非叶子节点段共享碎片区资源
-
分配逻辑:
if 段类型 == 叶子节点段: 分配完整区 else: 从碎片区抓取零散页(若碎片区不足则申请新区)
3. 段与区的映射关系
-
每个段通过 INODE Entry结构维护其管理的区列表:
struct inode_entry { space_id_t space_id; // 所属表空间 list_t free_extents; // 空闲区列表 list_t full_extents; // 已满区列表 list_t frag_extents; // 碎片区引用 };
5. 页的结构
5.1 页大小设置
通过参数innodb_page_size
配置(4K/8K/16K/32K/64K),需在数据库初始化时设置:
[mysqld]
innodb_page_size=32K
5.2 页的分类
页类型 | 标识值 | 用途 |
---|---|---|
数据页 | 0x45BF | 存储表记录 |
索引页 | 0x0002 | 存储B+树非叶节点 |
Undo页 | 0x0002 | 存储事务回滚日志 |
XDES页 | 0x0003 | 管理区分配状态 |
段信息页 | 0x0004 | 存储INODE条目 |
5.3 页头/页尾结构
5.3.1 ⻚头 - File Header
- ⻚号: FIL_PAGE_OFFSET 占⽤ 4Byte ,相当于⻚的⾝份证号,通过这个⻓度可以计算出每个InnoDB表中最多可以拥有 2^(48)-1约42亿 个⻚,表空间第⼀个⻚编号从0开始,之后的⻚号分别是1,2,3…依此类推,具体⻚的偏移量计算公式为:⻚号 * 每⻚⼤⼩;那么按照每个⻚默认16KB⼤⼩计算,⼀个表空间最⼤容量为 2^(48) * 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 ,⽤于⻚的完整性校验
5.3.2 ⻚尾 - File Trailer
- 最近⼀次修改的LSN
- 校验和:对应⻚头中的校验和
如果在数据传输的过程中数据丢失或异常中断,导致⼀个数据⻚不完整就可以通过⻚头和⻚尾的校验和进⾏验证,验证算法默认使⽤ CRC32。
5.4 页主体结构
5.4.1 数据行格式(重点)
| 变长字段长度列表 | NULL标记位 | 头信息 | 主键值 | 事务ID | 回滚指针 | 列数据...
1. 变长字段长度列表
InnoDB的变长字段长度列表是行记录中用于存储可变长度字段实际占用字节数的数据结构,主要处理:
- 可变长度类型:
VARCHAR
、VARBINARY
、TEXT
、BLOB
等 - NULL值标记:与NULL值列表配合使用
基础结构
| 字段1长度 | 字段2长度 | ... | 字段N长度 | 实际数据...
- 长度表示:
- 1字节:当长度 ≤ 127字节
- 2字节:当长度 > 127字节(最高位为标志位)
- 存储顺序:逆序排列(字段N的长度在最前面)
text、blob特别处理
当字段使用text、blob,并且数据量非常大时,真实数据就不会存储在该行了,而是存储在溢出页中,真实数据区使用20个字节溢出指针存储位置信息
溢出指针:
| 空间ID(4字节) | 溢出页号(4字节) | 偏移量(8字节) | 数据长度(4字节) |
2. NULL标记位
NULL值列表是InnoDB行格式中专门标记NULL字段的位图结构,主要解决:
- 空间优化:避免为NULL值分配存储空间
- 查询加速:快速判断字段是否为NULL
存储规则
特性 | 说明 |
---|---|
存储位置 | 位于行记录的起始部分,在变长字段长度列表之后 |
位图大小 | 每个字段占1bit,按8bit对齐(不足补0) |
标记方式 | 1 表示NULL,0 表示非NULL |
字段顺序 | 按照列定义的逆序排列(与变长字段长度列表一致) |
3. 头信息(40BIT)
deleted_flag
:删除标记min_rec_flag
:用于标识B+树非叶子节点中的最小边界记录,其具体表现为:- 当值为
1
时:表示该记录是当前非叶子节点中的最小目录项记录 - 当值为
0
时:普通记录或叶子节点记录
- 当值为
n_owned
:目录槽中行数,只有槽的最后一行该数据才有效heap_no
:记录堆序号(从2开始)record_type
:记录行类型,0=普通记录,1=非叶节点记录next_record
:下一行的地址偏移量
4. 主键值、事务ID、回滚指针
这三项是InnoDB数据行默认生成的
主键值(Primary Key)
数据行根据主键值进行排序,方便顺序IO,提高查找效率和页目录的使用
- 存储位置:紧跟在记录头(Record Header)之后
- 存储规则:
- 如果表定义了主键,直接存储主键值
- 如果没有主键,但存在非NULL唯一索引,存储第一个唯一索引的值
- 如果都没有,InnoDB自动生成6字节的
DB_ROW_ID
作为隐藏主键
事务ID(DB_TRX_ID)
- 作用:标记最后修改该行的事务
- 存储位置:主键值之后
- 大小:6字节
- 特性:
- 用于MVCC(多版本并发控制)
- 事务开始时从全局事务ID生成器分配
回滚指针(DB_ROLL_PTR)
-
作用:指向Undo Log中的历史版本
-
存储位置:事务ID之后
-
大小:7字节
-
结构:
| Undo Log位置 (4字节) | 历史记录序号 (2字节) | 标志位 (1字节) |
5.4.2 首尾记录标识
Infimum
:虚拟最小记录(heap_no=0)Supremum
:虚拟最大记录(heap_no=1)
5.4.3 页目录查询
- 每组4-8条记录建立槽位(Slot)
- 二分查找定位槽位后线性搜索
页目录的作用:
每个槽存放该组中的最后一个数据行的主键值,通过主键值二分查找页目录,查到数据行在哪一个槽中,读取该槽的最后一个数据行的头信息中的n_owned得到该槽中的行数,然后遍历该组记录。
5.5 事务信息存储
- 事务ID(db_trx_id):6字节
- 回滚指针(db_roll_ptr):7字节
- 通过
DB_ROLL_PTR
可追溯数据历史版本
6. 行结构
6.1 行格式对比
格式 | 特性 | 适用场景 |
---|---|---|
COMPACT | 768字节前缀存储 | 兼容老版本 |
DYNAMIC | 纯指针存储溢出页 | 默认推荐 |
COMPRESSED | 支持透明压缩 | 存储优化 |
6.1.1 查看行格式
SHOW TABLE STATUS LIKE '表名'\G
-- 或
SELECT ROW_FORMAT FROM information_schema.TABLES
WHERE TABLE_NAME='表名';
6.1.2 指定行格式
CREATE TABLE t1 (id INT) ROW_FORMAT=DYNAMIC;
ALTER TABLE t1 ROW_FORMAT=COMPRESSED;
6.1.3 DYNAMIC行特性
- 溢出处理:当行超过页大小时,仅存储20字节指针
- NULL优化:专用位图标记NULL列(每列1bit)
6.1.4 变长字段存储
- 长度≤127字节:1字节存储长度
- 长度>127字节:2字节存储长度
- TEXT/BLOB:始终使用溢出页
6.1.5 溢出指针结构
struct overflow_ptr {
uint32 space_id;
uint32 page_no;
uint64 offset;
uint32 data_len;
};
6.1.6 格式差异对比
特性 | REDUNDANT | COMPACT | DYNAMIC |
---|---|---|---|
行头 | 6字节 | 5字节 | 5字节 |
NULL处理 | 字段固定长度 | 位图标记 | 位图标记 |
溢出处理 | 部分存储 | 部分存储 | 全溢出 |