目录
InnoDB简单了解
InnoDB的特性
InnoDB架构
InnoDB存储引擎创建表的数据文件
MySQL存储结构
表空间文件
用户数据在表空间中存储方式
使用页数据存储单元的原因
数据页
区
表中数据少时如果避免空间浪费
区组
段
页
数据行的组成
快速定位数据在页中位置
事务和索引信息在信息中记录方式
数据页完整结构
编辑
行结构
InnoDB支持的行格式
数据区(额外信息+真实数据)
额外信息 - 头信息
额外信息 - NULL列表
额外信息 - 变长字段
MySQL存储结构总结
InnoDB简单了解
InnoDB的特性
数据库的设计主要就是围绕安全和性能,InnoDB在设计的时候就考虑到巨大数据量时候的性能。InnoDB支持事务、回滚并且有崩溃修复能力,通过多版本并发控制减少锁定,与此同时还支持外键约束,通过缓冲池在主内存中缓存数据从而提高查询性能
综上所述,InnoDB凭借其强大的事务支持、并发性能、数据安全和完整性,成为MySQL的首选引擎。在保证数据安全性和高性能的场景下优势十分突出。
主要优点
- 事务支持
- 支持ACID(原子性、一致性、隔离性、持久性)事务的存储引擎,从而保证数据的完整性和一致性
- 支持提交、回滚和崩溃恢复
- 行级锁(高并发)
- InnoDB的行级表机制,可以精细的锁定被修改的数据行,而不是整个表。从而使得多个事务能够在同一表上进行操作,而不会进行相互阻塞,从而提升高并发的性能
- MyISAM使用的是表级锁,不适合高并发写入场景
- 外键支持
- 当涉及到多表关联操作时,外键约束十分重要
- 外键的存在可以确保数据库中的关联数据一致,从而减少数据异常的风险
- 崩溃恢复能力
- InnoDB使用重做日志(Redo Log)和撤销日志(Undo Log)记录数据操作,当数据库崩溃或意外关闭时,可以利用日志恢复未完成的事务,保障数据安全
- MVCC(多版本并发控制)
- 允许多个事务同时读取数据的不同版本,从而实现读写分离,进一步提高并发性能
- MySQL5.5版本之后就使用InnoDB作为默认引擎,5.5版本之前则是MyISAM
InnoDB架构
InnoDB的架构主要由内存结构和磁盘结构组成。内存结构主要用于缓存和管理数据,加速数据库操作,其中主要有缓冲池、自适应哈希、日志缓冲区。磁盘结构用来保证数据持久性,其中主要有系统表空间、独立表空间、通用表空间、双缓冲区机制、回滚日志、临时表空间
内存结构
- Buffer Pool(缓冲池):用于缓存表的数据页、索引页等。Buffer Pool 是 InnoDB 的核心组件,可以显著提升查询性能。访问数据时,InnoDB 优先从 Buffer Pool 中读取,如果数据不在缓冲池中,则从磁盘加载到缓冲池再访问
- Adaptive Hash Index:在缓冲池中维护的一个自适应哈希索引。当 InnoDB 检测到某些索引访问频繁时,会自动将它们放入自适应哈希索引中,以加快访问速度
- Change Buffer:记录对非唯一二级索引的修改操作,用于减少写磁盘的频率。修改暂时缓存在 Change Buffer 中,稍后批量写入磁盘
- Log Buffer(日志缓冲区):用来暂存事务日志,避免每次事务都直接写入磁盘,从而提高性能。日志缓冲区的内容会定期刷新到磁盘
磁盘结构
- System Tablespace(系统表空间):存储 InnoDB 的元数据以及全局的数据字典和 Change Buffer。在系统表空间中,所有数据库的基本信息和一部分数据都会被存储
- File-Per-Table Tablespaces(独立表空间):如果启用了
innodb_file_per_table
选项,每个表的数据和索引可以单独存储在.ibd
文件中。这种方式在数据恢复和管理上更加灵活 - General Tablespaces(通用表空间):用户可以创建多个通用表空间,不同表可以共享一个通用表空间,从而实现更灵活的表空间管理
- Doublewrite Buffer Files:InnoDB 采用双写机制(Doublewrite)来防止数据损坏。数据页在写入表空间前,会先写入双写缓冲区文件,确保在数据库崩溃时可以恢复
- Undo Tablespaces:用于存储事务的 Undo 日志,支持事务回滚和 MVCC(多版本并发控制),以实现一致性读和回滚功能
- Redo Log:记录数据的变更操作,支持事务的崩溃恢复。当数据库重启时,可以利用 Redo Log 恢复未提交的事务
- Temporary Tablespaces(临时表空间):用于存储临时表的数据,数据在会话结束或服务器关闭时会自动清除
数据访问流程分析
当应用程序访问数据库时,数据首先从 Buffer Pool 中读取,若数据不在 Buffer Pool 中,才会从磁盘读取数据并加载到 Buffer Pool。同时,所有的数据变更会先记录在 Log Buffer,然后定期写入 Redo Log 和 Doublewrite Buffer 中,以确保数据的安全性
为什么设计成内存结构和磁盘结构两个部分
InnoDB架构设计成内存的和磁盘的主要目的是性能和数据持久化。内存结构主要用于加速数据访问和减少磁盘I/O,提高系统性能;磁盘则是主要用于确保数据持久化和支持恢复机制,从而确保数据的安全性
InnoDB存储引擎创建表的数据文件
数据目录的创建和管理(重点)
当创建一个新的数据库时,MySQL会在默认的数据目录中(/var/lib/mysql/)下创建一个子目录存储该数据库中的所有表文件。也就是如果创建了一个名为mydatabase的数据库的时候,会在/var/lib/mysql/目录下生成一个mydatabase目录,该目录下有所有mydatabase数据库在表文件和其他文件
实践验证
CREATE DATABASE mydatabase_test;
USE mydatabase_test;
CREATE TABLE mytable (
id INT PRIMARY KEY,
name VARCHAR(50)
) ENGINE=InnoDB;
.ibd 文件是InnoDB特有的文件格式,其中包含了表的数据页、索引页、insert Buffer、undo信息以及其他内部数据
MySQL5.7以后的版本为每个表生成独立的表空间,可以通过系统变量innodeb_file_per_table[=on][off]来进行控制
MySQL存储结构
表空间文件
什么是表空间文件
InnoDB的表空间文件就是用于高效管理表和索引的数据存储。通过系统表空间、独立表空间和通用表空间等不同类型实现,InnoDB提供了灵活的存储管理方式
表空间和表空间文件之间含义和关系梳理
表空间是一个逻辑上的概念,主要用于划分数据库的存储区域。而表空间文件则是实际的存储文件,实现了表空间的物理存储。数据库的管理之中的,表空间的划分可以更灵活的管理数据,表空间文件则决定了数据在磁盘上的具体存储位置。
通过图书馆中事例梳理其中的关系
- 数据库的表空间系统(图书馆)
- 图书馆就很像数据库的表空间系统,主要用于组织和管理所有的书籍(那么这些书籍对应的也就是数据表)
- 每个图书馆又有很多区域,存放不同类型的书籍。数据库的表空间系统也就包含多个表空间,用于管理数据表
- 表空间(书架)
- 表空间就像图书馆的书架,划分了不同的存储区域,用于存放特定类别的数据
- 每个表空间对应一个或者多个表数据,提供了分离和管理数据的方式
- 表空间文件(书架上的隔板)
- 首先书架上的隔板的作用就是规定一个小区域中放有多少书籍
- 表空间文件就像这些隔板,是表空间的实际存储单位。每个表空间可以包含一个或者多个表空间文件,每个文件存放一定数据量
- 最后表空间文件本质就使得数据库可以高效的存储和检索数据
- 数据表(书籍)
- 每一本书也就对应着一张真实的数据表
- InnoDB中,一个表的数据和索引就像图书馆中的一本书籍,其存储在不同的表空间中,并且通过表空间文件实现物理存储
按照上述逻辑理解不同表空间的含义
- 系统表空间:可以理解为一个大型书架,用来存储整个图书馆的目录、索引和一些常用书籍。这个书架是固定的,始终存在
- 独立表空间:可以看作是每本书自己拥有的专属书架。每本书的数据和索引都单独存储在自己的隔板上,便于单独管理和搬运
- 通用表空间:类似于一个共享书架,多个相关的书籍可以放在同一个书架上。如果某些书籍是相同主题的,图书管理员可以选择将它们放在一个通用书架上,以便分类管理
- 临时表空间:就像图书馆的临时书架,用于临时存放一些要归还的书籍。当这些书籍不再需要时,就可以清空该书架
用户数据在表空间中存储方式
数据是按照数据行的方式存储在对应表空间文件中,表空间为了高效的管理这些数据,表空间内部还有段、区组、区、页、数据行,其中页是InnoDB磁盘管理的最小单位
若干数据行组成了页, 多个的页组成了区,多个区组成了区组,多个区组组成了段,多个段组成了表空间
具体含义分析
- 表空间:存储数据的逻辑存储结构,InnoDB存储引擎中,最终所有的数据都会存储在表空间
- 段:表空间进一步划分为段。段是数据的逻辑存储单位,不同类型的数据被存储在不同的段中
- 区组:多个区组成一个区组,区组进一步管理段中的数据。区组是表空间一个更高层次的逻辑结构
- 区:区是一个分配单位,由多个页组成。InnoDB中,一个区是由64个连续的页组成(每个页的默认大小是16KB)所以一个区的大小为1MB
- 页:InnoDB存储引擎中管理磁盘的最小单位,每个页默认大小为16KB,页中可以存放数据行和索引数据
- 数据行:页进一步存储数据行,每一行代表一条记录
使用页数据存储单元的原因
InnoDB存储引擎选择页为最小存储单位,主要是为了优化磁盘I/0、提高内存缓存的利用效率、简化数据管理并提高并发性能。页的使用目标就是让InnoDB在性能和数据一致性上达到平衡
减少I/O
设定固定的页大小可以让InnoDB每次操作批量的读取或者写入数据块,这样就比行读取或者写入更加有效
数据库读取的时候尽量选择顺序读取,而不是随机读取。而页的设计让数据库可以一次性读取较大快的数据,这样就提高了IO效率
内存缓存的有效管理
页缓存策略:InnoDB的Buffer Pool将页作为缓存单位,将磁盘上的数据页加载到内存中的Buffer Pool中,这样数据库就可以将频繁访问到的数据,都保存到内存中,提高数据的访问数据
Buffer Pool的设计也使得内存上的数据页与磁盘上的数据页保持一致,有效的提高了数据的读取或者写入速度,减少了对磁盘的直接访问次数
方便空间管理
InnoDB使得页作为最小的分配和回收单位,从而使得存储空间的管理更加高效。其对于新插入的数据,InnoDB页可以方便的为其分配新的页,而不必要在插入的每一行都去查找和分配空间
InnoDB对页进行整理和优化,减少数据在物理存储上的碎片,提高空间利用率
InnoDB 使用 B+ 树索引来存储表数据,而 B+ 树的节点可以与页大小匹配,从而可以在每个页中存储完整的索引节点。这样可以减少树的高度,加速索引的查找效率
什么事局部性原理
局部性原理在数据库设计中主要包括时间局部性和空间局部性两个方面,其主要目的都是为了减少磁盘I/0、提高缓存命中率、提升数据的访问性能
时间局部性
时间局部性就是最近访问的过的数据在不久的将来可能还会被再次访问到。在InnoDB中主要表现在两个方面,Buffer Pool缓存和自适应哈希
- Buffer Pool缓存:InnoDB将最近访问的页缓存到Buffer Pool中,该缓存池在内存中,当某页中的数据访问到时,会将整页的数据都加载到内存中,然后在后续的访问中优先读取该页,从而避免重复访问磁盘
- 自适应哈希:InnoDB会去检测到某些数据或者索引的访问频率较高,自动为这些的数据创建哈希索引。从而使得高频访问的行数据通过索引直接定位,提高查询速度
空间局部性
空间局部性就是如果一个数据被访问了,那么它存储临近位置的数据也可能被访问到
- 页作为最小存储单位:页是InnoDB中最基本的存储单位,每个页游16KB,当访问某一个数据行的时候,该页就会加载到内存,这样就拿到了与之空间上相连的数据
- B+树结构和聚簇索引:InnoD中使用的是B+树结构存储表的主键索引,数据行按照主键顺序存储在叶接地那中,保证数据的物理顺序。
- 因为B+树的节点是以页为单位进行存储的,索引相邻的数据行在物理上页存储在相邻页中
数据页
页大小
InnoDB的默认页大小是16KB,页大小的选择直接影响到数据存储效率和I/O性能,设置合适的页大小可以保证磁盘的访问效率并适应其需求
数据页的结构
每个数据页占用指定的大小,即使页中没有实际的数据,也会保留其存储空间,这样的做的目的是为了保证其物理组织的稳定性
数据页主要有三个部分组成
- 页头:记录页的元数据,例如页类型、页编号
- 数据行:数据页的主要部分,数据行结构一般是与索引B+树节点相适应
- 页尾:主要用于检验页的完整性,防止页损坏
数据页的类型和用途
数据页和索引页都是支持B+树结构的,B+树是InnoDB的主键和二级索引实现的基础
- 数据页:用于存储表中实际的数据,也就是每条记录中的具体信息
- 索引页:存储索引数据,包括主键索引和二级索引
- Undo页:存储回滚信息
数据页的管理和性能优化
数据页遵循局部性原理,参考上文;自适应哈希;Buffer Pool;
页分配的连续性
数据页通常按区分配,一个区由 64 个连续的页组成(即 1MB),这种分配方式有助于保证数据的物理连续性,减少磁盘的随机 I/O
页的分配策略确保了数据在物理上的连续性,有助于顺序访问的性能优化,特别是在执行扫描操作或批量查询时表现更优
页损坏检测和数据完整性
数据页包含页尾用于数据完整性的效验,防止页在传输和存储过程中损坏。通过页尾效验机制,InnoDB可以在数据页发生损坏的时候检测并进行处理
区
不同页在磁盘中的存储是否连续
首先InnoDB的页面在磁盘上不一定是连续的。因为InnoDB使用一个聚簇索引结构来存储数据,该结构不要求数据页在磁盘上连续存放。页面的分配是根据数据的插入、更新与删除等操作进行的,因此数据页可能会分散在磁盘的不同位置。InnoDB使用表空间管理这些页面,而表空间中的页面并不保证是连续存储
如果页不是连续的那么对效率有什么影响
磁盘寻址时间加长
机械硬盘访问数据是通过磁头在磁盘上移动。如果是连续存储的话,磁头就可以在一个较小的区域移动,这样就减少了寻址延迟。相反如果是非连续存储,磁头就需要频繁跳转,增加了寻址延迟。
固态硬盘,尽管没有物理磁头的移动,但是由于内存块的擦写特性,存储非连续的页面仍然会增加控制器的开销,所以需要频繁的进行块的重新分配和管理
磁盘的缓存效率降低
操作系统和存储引擎都会利用磁盘的预读取功能来提高性能(也就是局部性原理)。如果是连续存储,那么能够有效利用预读取机制,读取一个数据页的时候,顺带读取相邻的页面,从而减少磁盘的I/O操作。如果是非连续存储,那么预读取就无效,就会增加I/O操作
增加内存开销
如果数据分布不连续,那么操作系统和数据库引擎就会将这些不连续的数据页分散存储在不同内存中,这也就意味着CPU需要频繁的进行内存访问,增加了内存访问的复杂度。
连续存储时,数据页可以加载到内存中一个较大的缓冲区,减少内存访问开销。非连续存储的时候,数据页分布在内存的不同位置,就会增加访问成本
InnoDB如果保证连续性
InnoDB通过划分区来实现存储的连续性,每个区由64个连续的页面组成。通过将频繁访问的区数据加载到内存中的缓冲池中,InnoDB从而减少磁盘的随机访问次数,提高读取效率。所以分区和缓冲机制是InnoDB保证连续性的关键
总结
InnoDB通过区组织数据页,尽可能减少随机IO,提高查询效率。页在区内相邻的时候,磁盘顺序I/O;页在区内但是不相邻,减少磁盘移动,同样提高效率;页如果在不同的区,那么需要发生随机I/O,无法提高效率
表中数据少时如果避免空间浪费
分析
创建表时候初始化页数量:因为当创建新表且数据量未知的事后,InnoDB不会直接分配一个完整的区(1MB),而是只分配少量的初始页(5.7版本是6个初始页,其他版本存在7个,存在变化)。使用该方式避免一开始分配大块空间的浪费
碎片区的使用:初始的零散页(例如刚开始创建新表的时候,会分配7个页,等待体积扩大的时候,再对其进行合并)会放置在表空间中的一个碎片区的地方,随着数量的增加,InnoDB会申请新的页来存储数据,然后将这些页放入碎片区中。
碎片区到完整区:碎片区的页数达到32页的时候,InnoDB就会按照1MB也就是一个完整区来分配空间,从而实现高效存储数据
总结反思
通过使用零散页和碎片区的策略,InnoDB 能够在数据量较少时减少空间的浪费,并随着数据量的增加逐步转为完整区的分配方式,从而实现存储空间的优化
区组
分析
区组结构
每个区组管理256个区,每个区大小为1MB,所以每个区组管理的空间为256MB。区组内的区记录信息被集中管理,可以快速定位区的位置和状态,从而提高跨区访问效率
区组条目信息(区组前几个页面记录)
这些信息的主要目的就是快速找到所需要的区,同时识别区的使用状态,减少查找时间
- File Space Header:记录表空间和区组中的条目信息
- Extent Descriptor(XDES):每个区的描述信息,记录区的分配和使用情况
- Insert Buffer Bitmap:与 Change Buffer 相关的信息,用于加速对数据的更改操作
区组对页管理分析
首先区组的前几个页存储了区和页的信息,从而使得系统可以快速定位到某个页所在的区。InnoDB通过区组结构,可以快速定位行数据所在的区组和区,从而减少了跨区I/O,减少磁盘寻址的开销
然后如果需要跨区访问,系统可以通过区组条目迅速查询到下一个区的位置,从而避免遍历所有区低效率。
查询数据在不同区的时候,具体是利用区组的偏移和映射关系,通过索引或条目表定位到具体区的位置,从而提高访问速度
总结
通过区组的设计,InnoDB 可以高效地管理和访问大量的区,尤其在数据查询需要跨区时,能够通过区组条目迅速定位到目标区,从而减少随机 I/O,提升查询效率。这种管理方式在面对大数据量和复杂查询时尤为有效
段
分析
段主要分为两类
- 叶子节点段:主要用于存储B+树的叶子节点,这些节点存放了实际的数据行
- 非叶子节点段:用于存储B+树的非叶子节点,这些节点主要就是用来保存索引信息以及指向其他节点的指针
段的物理存储分析
- 段是表空间(.ibd文件)中的逻辑结构,其是由数据页和索引页组织在一起,目的就是优化数据访问和管理
- MySQL表空间由多个区组成,而每个区中又有多个页组成,段就是负责这些区和页,从而确保数据和索引的有效存储
- 叶子节点段:包含存储实际数据的页,这些数据页存放了表中的行数据。叶子节点段在B+树的最底层,因此也就是最终实际存储数据的地方
- 非叶子节点段:包含索引页,主要用于存储索引信息和指向其他节点的指针,这些段主要就是用于快速查找数据的,而不存储实际的数据
B+树和段的关系
因为MySQL InnoDB引擎就是通过B+树来实现索引的。因为B+树的结构让查找、插入和删除效率高,而通过将数据和索引分开存储在不同的段,就可以进一步的提高性能
- 叶子节点段存储B+树的叶子节点,这些节点又包含了表中的实际数据
- 非叶子节点段则存储在B+的非叶子节点,主要用于索引的维护和查找操作
总结
MySQL InnoDB中,段是逻辑上的一种存储区域,主要用于管理物理空间的分配,按功能分为“叶子节点段”和“非叶子节点段”,作为B+树索引中的叶子、非叶子节点,从而进一步提升查询效率
拓展问题
1. 上述操作是否在内存中进行?
数据库中大多数操作都是在内存中完成的,最后这是在必要的时候才将修改后的数据持久化到磁盘上
InnoDB引擎在处理数据的时候,首先所有的操作都是先在内存中进行,需要持久化的时候,才会将修改的数据刷新到磁盘上。使用该机制的目的在于提高数据的处理速度,本质上还是内存的速度是快于硬盘
2. 查询数据的时候MySQL是否会一次性将表空间中的数据全部加载到内存?
MySQL只会加载查询所需要的数据页,不会加载整个表空间
因为表空间非常大,如果全部加载会占用大量内存空间,导致系统资源耗尽。使用InnoDB存储引擎的时候,MySQL查询数据会根据表空间中定义的索引结构,找到所需要的数据页,然后将其加载到内存中
3. 没查询一次数据都需要进行一次I/O嘛?
如果数据已经在内存中,MySQL查询的时候不需要再次访问磁盘;否则,需要进行磁盘I/O
因为MySQL使用的缓冲池,当查询的时候如果所需要的数据在缓冲池中,那么直接从内存中读取数据即可,不需要每次都进行磁盘I/O;如果数据不在内存中,则会触发磁盘I/O,将所需要的数据加载到内存中,然后返回查询结果。
页
页的大小是否可以设置
可以通过配置innodb_page_size来调整页的大小,从而优化MySQL的存储和性能。但是需要根据实际应用场景来选择合适的页大小,并在数据库初始化之前设置好相应的参数
分析
InnoDB中默认的页大小是16KB,但是大小并非固定,可以自行设置,一般是操作系统数据块的整数倍。
调整页的大小可能对数据库的性能产生影响。较小的页适合大量小数据行的表,可以减少内存浪费;较大的页包含大数据行的表,可以减少磁盘I/O次数,从而提高大数据量查询性能
InnoDB页的主要分类,以及重点关注的页
重要的页有数据页,索引页和undo页。数据页需要理解其结构和管理形式以帮助优化数据存储和查询性能;索引页重点学习其B+树的应用,有助于提高查询效率;undo页则需要了解事务回滚和多版本控制,对数据库的一致性和事务管理很重要
页头和页尾的主要作用
页头主要用于管理和维护页的信息,例如页类型、页编号、链表指针等,主要用于快速定位和访问页数据
页尾用于数据的完整性效验,确保页在读写过程中不被损坏,同事保证日志序列号一致性,帮助数据库在崩溃后恢复数据
- 页尾
- Checksum(校验和):
- 用于数据完整性校验,确保页在从磁盘读取时未被损坏。InnoDB 在写入页时会计算校验和,在读取页时重新计算以验证数据的完整性。
- Log Sequence Number(日志序列号):
- 再次记录日志序列号,用于确保该页的修改顺序与事务日志一致,帮助数据库在崩溃后恢复数据。
- Page End Signature:
- 标记页的结束,防止页被部分覆盖或损坏时的数据丢失
- Checksum(校验和):
- 页头
- Page Type(页类型):指示当前页的类型(例如数据页、索引页、Undo 页等)。
- Page Number(页编号):每个页都有一个唯一的编号,用于在表空间中标识和定位该页。
- Page Offset(页偏移量):页在表空间文件中的偏移量,用于快速定位页的位置。
- Log Sequence Number(日志序列号):记录该页最后一次修改时的日志序列号,用于数据一致性和崩溃恢复。
- File Header(文件头):包含页所属的表空间信息。
- Prev Page 和 Next Page(前一页和下一页指针):在链表结构中用于链接相邻的页,主要用于 B+ 树的叶子节点页之间的快速导航。
- Free Space Offset(空闲空间偏移):指向页内可用的空闲空间,用于插入新数据时定位。
- Garbage Space(垃圾空间):指示页内被删除但尚未回收的空间大小。
数据行的组成
分析
额外信息分析
- 可变长度字段长度列表:用于存储每个可变长度字段的长度。
- NULL 值列表:标识哪些字段的值为
NULL
。 - 5 字节的固定头信息区域,用于存储行的元数据。
- 删除标记:表示该行是否已被删除。
- 非叶子节点标志:用于 B+ 树的内部节点。
- 目录节点的行数 (
n_owned
):当前目录下管理的行数。 - 行在页内的位置 (
heap_no
):行在当前页面中的位置编号。 - 行类型 (
record_type
):表示该行的类型(普通行、B+ 树指针等)。 - 下一行地址偏移量 (
next_record
):指向下一条记录的偏移量,用于链接相邻的行。
实际数据部分
总结
数据行主要分为两个部分,一部分存储额外的信息,另一部分存储的真实数据。额外信息部分包含变成字段长度列表和NULL值列表两个大小不确定的区域,以及固定占5字节的头信息区域
拓展1:数据行是如何组织在一起的
每一行数据都有一个next_record字段,表示当前数据行到下一个数据的地址偏移量,这个偏移量指向下一行数据的起始位置,而不是额外信息部分。所以数据行之间可以紧密的链接在一起,从而形成一个单向链表结构
数据行是通过next_record链接成单向链表,实现数据在页内的高效组织。这种组织方式能够优化数据的插入、删除和查询操作,同时减少页内存储的碎片化。
拓展2:如果标识新页中的第一行和最后一行
首先最大行不存储实际数据,仅用于标记数据链表的结束,同样最小行页不存储任何实际的数据,仅用于表示数据链表的起始位置
在新创建的页中,最小行的next_record会直接指向第一条实际数据行,所有实际数据行通过next_record链接在一起,从而形成一个单向链表。最后一条数据行则指向最大行,而最大行的next_record为0,表示链表结束
InnoDB通过在新页中自动创建infimum和supremum两个特殊行,从而确保链表的起点和终点明确。
拓展3:当向一个新页插入数据时如何执行的
页内被已插入数据行占用的部分是用户数据区;页中没有被使用的空间则是空闲区,主要用于存放后续插入的数据行
InnoDB中当新页插入数据的时候,数据行通过next_record指针组成一个单向链表。新插入的实际数据行在最小行和最大行之间插入,同时保证主键的顺序。页内未使用的部分保留为Free Space,便于后续插入数据使用
快速定位数据在页中位置
分析
页内的数据是有序存储。因为InnoDB中的聚簇索引确保数据在页内是按照主键顺序排列的,其数据行在物理存储上也是有序。
页目录将页中的数据行分成多个组,每组中的数据是按照顺序排列的。页目录记录了每组的起始位置,当需要查找某个数据的时候,MySQL可以通过页目录快速定位到某个数据组,然后进而找到对应的数据行
页目录的查找中通过二分查找算法,提高下来。同时因为其是使用单向链表连接的,所以可以通过列表快速到达目标位置
总结
查找某个页中数据的时候,InnoDB并不是简单的一条条遍历数据行,而是通过页目录和二分查找相结合的方式,快速定位数据所在的位置。通过页目录和二分查找,InnoDB可以在O(log n ) 的时间复杂度内查找到数据
事务和索引信息在信息中记录方式
总结
数据页头中存储该信息,其中记录的有索引和事务信息,除此之外还记录了统计信息和位置信息
数据页完整结构
行结构
InnoDB支持的行格式
总结
InnoDB主要支持四种行格式,REDUNDANT冗余格式、COMPACT紧凑格式、DYNAMIC动态格式(默认格式)、COMPRESSED压缩格式
拓展1:如果查看当前数据库表应用了哪些行格式
通过系统变量查询
查看数据库中所有表
拓展2:如何指定行格式
通过全局变量设置默认行格式
SET GLOBAL innodb_default_row_format=DYNAMIC;
创建表的时候指定行格式
CREATE TABLE t1 (c1 INT) ROW_FORMAT=DYNAMIC;
拓展3:DYNAMIC格式由哪些部分组成
主要由两部分组成,一部分是存储真实数据的区域,另一部分是存储额外信息的区域
数据区(额外信息+真实数据)
分析*
额外信息
- 变长字段列表:记录变长字段的起始位置,用于定位变长字段的起始位置
- NULL值列表:记录哪些字段的值为NULL,节省存储空间
- 头信息:一些行的元数据
真实信息
- 主键列:用于唯一标识表中的每一行,注意有唯一键、复合键,还会为其自动创建一个默认的住键
- 如果表定义了主键列,则直接存储主键值。
- 如果使用复合主键,则主键按照定义的顺序依次排列存储。
- 如果没有主键但存在唯一键,InnoDB 会优先使用一个不允许
NULL
的唯一列作为主键。 - 如果表既没有主键也没有唯一键,InnoDB 会自动创建一个 6 字节的隐藏列
DB_ROW_ID
,为每行记录提供唯一标识符。
- 事务相关固定字段
- 6字节事务ID字段:记录创建或者最后一次修改记录的事件ID,便于支持事务和并发控制
- 7字节回滚指针:如果事务中该记录被修改,回滚指针指向这条记录的上一个版本,从而支持多版本并发控制
- 其他数据
- 除了主键和被标记为
NULL
的列外,表中的其他列的真实数据会按顺序依次存储在数据区域中
- 除了主键和被标记为
- NULL值不直接存储
- 对于允许NULL的列,InnoDB会将这些信息记录在行开头的NULL值列表中,避免存储额外空间
总结
额外信息 - 头信息
分析
头信息中存储的元数据主要是用于管理和优化数据结构的存储和访问
- 下一行地址偏移量
- 占用16位,记录下一行的偏移量
- 通过该偏移量,InnoDB将数据行链接成一个单向链表(即使物理位置改变,也可以快速定位到数据的位置),从而可以实现页内的行遍历
- 行类型
- 占用3位,表四数据的类型
- 主要有四种类型
0
:普通数据行,用于存储实际的数据。1
:目录索引行,通常用于标识索引的层次结构。2
:页内最小行(infimum
),是页内的一个标记行,表示页的起始。3
:页内最大行(supremum
),是页内的另一个标记行,表示页的结束。
- 行位置标识(heap_no)
- 占用13位,主要用于表示该行在当前页中的位置编号
- heap_no可以快速定位行的位置,便于对行的快速访问和更新
- 分组的行数
- 占用4位,用于表示分组中最后一行所拥有的记录数量
- InnoDB会对页中的行分组,从而加快查找速度
- 最小记录标记
- 占用1位,表示该行是否是某一索引层级中的最小值
- 如果标记为1,那么就表示该行是某一行索引的最小行
- 删除标记
- 占用1位,用于标记改行是否被删除
- 主要目的就是避免频繁的数据移动,提高删除操作效率,在恢复数据的时候,直接修改删除标记即可
- 预留位
- 未来拓展使用
总结
拓展:删除一行数据的时候InnoDB内部如何实现
删除一行记录的时候,InnoDB主要会在头信息中标记删除,进行逻辑删除;写入撤销日志,记录删除前的信息,避免回滚;更新事务日志,记录删除操作以支持故障恢复;清除操作,延迟物理删除,释放空间;调整B+树索引,维护索引的正确性。
标记删除
- 删除一条数据的时候,并不会立即在物理上删除该记录,而是将删除标记变成1
- 虽然此时看起来已经删除,但是数据仍然是在数据页中,如果事务回滚的时候还是可以恢复记录的
写入撤销日志
- InnoDB将删除操作写入撤销日志中,如果日志回滚则根据撤销日志恢复记录
- 撤销日志中还可以用于实现多版本并发控制,未提交的事务中,其他事务还是可以阅读该数据的
更新事务日志
- InnoDB会将删除操作同步记录的重做日志(redo Log),用于系统崩溃的时候故障恢复
清理操作
- 当事务提交后并且不会有其他事务需要访问已经删除版本的时候,InnoDB会通过清理线程进行物理清理
- purge 线程(专门负责清理的线程)会遍历已经标记的记录然后将其从数据页中移除
调整B+树索引
- 删除后调整B+树的索引结构
额外信息 - NULL列表
分析
NULL值列表是以位图形式表现的,位图中每一位对应的是一个允许为NULL的列。如果该位的数值是1,那么表示该列的值为NULL;如果该位的数值的0,那么就表示该列有非NULL值。
例如一个表有8列,且第2列和第5列的数值为NULL,那么对应的NULL值列表就是01001000,表示这两列的数值为NULL
总结
NULL值列表是MySQL行存储结构中的一个位图,用于标记哪些列的值为NULL,从而节省存储空间和提高查询效率。通过NULL值列表,MySQL可以快速判断是否为NULL,而不必检查的具体的数据内容
额外信息 - 变长字段
分析
影响列实际长度因素
- 列的类型是一个可变长度类型
- 字符编码集
- 例如utftm4,最多4个字节表示一个字符,那么英文、中文和表情占用的字节就不相同
常见的可变字段
- varchar \ varbinary \ text \ blob
- 使用了例如utf-8 \ gbk等可变长字符集的char类型
存储原理
- VARCHAR存储规则
- 长度限制分析:针对于VARCHAR的长度上限取决于字符集的字节数,例如使用utf8mb4的时候,一个字符需要占用最多4个字节;
- 每个VARCHAR字段最多存储65535字节;又因为InnoDB的行长度限制为16384字节(16KB页),所以实际存储中单个VARCHAR字段的长度上限则是16383字节
- 如果在开辟空间的时候超过了16KB的上限,就会因为超长字段而导致错误(下面分析解决办法)
- 额外存储:VARCHAR需1-2个字节的额外空间存储字节长度
- 实际存储位置:数据小的事后,直接存储在主数据页;数据长度较大的时候,则数据可能会溢出存储在其他溢出页中
- 长度限制分析:针对于VARCHAR的长度上限取决于字符集的字节数,例如使用utf8mb4的时候,一个字符需要占用最多4个字节;
VARCHAR(20000)报错原因分析
- 可变最大字节数为 4*20000 = 80000字节,超过了单行的存储限制
- 因为InnoDB单行存储限制是16KB(16384字节),所以即使有溢出机制,字段仍然需要存储在主表中,超长字段直导致错误
- 解决方法,使用TEXT或者BLOB字段存储大文本数据;这些字段类型会将超大数据存储在溢出页中,仅在主页数据中保留指针
总结
变长字段列表中记录了数据行中所有变长字段的实际长度,目的在真实数据区域,可以根据列的长度进行列与列之间分割,从而可以读取完整的数据
没有溢出的字段存储真实长度;溢出的字段存储指向溢出页的指针,上图中列2就是数据的长度,列3则存储的就是一个指针
拓展1:如何记录变长字段的实际长度
字符集与最大字节长度
- M表示的就是VARCHAR()中的数据,N则是使用字符在内存中占据的字节,例如ASCII占据1个字节,UTF8MB3占据3个字节
- 最大字节长度的公式为M*N,如果字段使用
VARCHAR(10)
,且字符集是UTF8MB4
,则该字段的最大可能长度为 10 × 4 = 40 字节
实际存储的内容长度决定如何具体存储长度
- 根据数据的字节数决定,字节数 <= 127 那么就使用1字节的值表示其长度;如果大于 127字节,那么就用2个字节的值表示其长度
拓展2:读取长度时如何处理粘包问题
换句话说也就是说在读取变长字段长度的时候,如果确定读取一个字节还是两个字节
任何时候首先读取一个字节,然后判断这个字节的高位是否为0,如果是0的话就表示当前用一个字节表示长度,如果是1则表示当前两个字节表示长度,那么就继续读一个字节,让后将两个字节合并在一起进行解析,得到该字段真实的使用字节数,同时第二个比特位表示是否使用溢出页
例如字段长度为60字节
- 第 1 字节:
00111100
(十进制 60,最高位为0
) - 最高位为
0
,表示长度信息仅占 1 字节 - 解析结果:字段长度为 60 字节
例如字段长度为200字节
- 第 1 字节:
10000001
(最高位为1
,表示需要读取第 2 字节) - 第 2 字节:
01001000
- 去掉第 1 字节的最高位,将其与第 2 字节合并
- 第 1 字节低 7 位:
0000001
- 第 2 字节:
01001000
- 第 1 字节低 7 位:
- 合并得到二进制:
0000001 01001000
(十进制 200) - 解析结果:字段长度为 200 字节