B-Tree、B+Tree、Hash

为什么MySQL索引要用B+tree

前言

当你在遇到了一条慢  SQL  需要进行优化时,你第一时间能想到的优化手段是什么?

大部分人第一反应可能都是添加索引,在大多数情况下面,索引能够将一条  SQL  语句的查询效率提高几个数量级

索引的本质:用于快速查找记录的一种数据结构

索引的常用数据结构

  1. 二叉树
  2. 红黑树
  3. Hash 表
  4. B-tree  (B树,并不叫什么B减树😁)
  5. B+tree
    数据结构图形化网址:Data Structure Visualization

索引查询

大家知道  select * from t where col = 88  这么一条  SQL  语句如果不走索引进行查找的话,正常地查就是全表扫描:从表的第一行记录开始逐行找,把每一行的  col  字段的值和 88 进行对比,这明显效率是很低的。
图片
而如果走索引的话,查询的流程就完全不一样了(假设现在用一棵平衡二叉树数据结构存储我们的索引列)

此时该二叉树的存储结构(Key - Value):Key 就是索引字段的数据,Value 就是索引所在行的磁盘文件地址。

当最后找到了 88 的时候,就可以把它的 Value 对应的磁盘文件地址拿出来,然后就直接去磁盘上去找这一行的数据,这时候的速度就会比全表扫描要快很多。
图片
实际上 MySQL  底层并没有用二叉树来存储索引数据,是用的 B+tree(B+树)

为什么不采用二叉树

假设此时用普通二叉树记录  id  索引列,我们在每插入一行记录的同时还要维护二叉树索引字段。
图片

此时当我要找  id = 7  的那条数据时,它的查找过程如下:
图片
此时找  id = 7  这一行记录时找了 7 次,和我们全表扫描也没什么很大区别。显而易见,二叉树对于这种依次递增的数据列其实是不适合作为索引的数据结构。

为什么不采用 Hash 表

Hash 表:一个快速搜索的数据结构,搜索的时间复杂度 O(1)
Hash 函数:将一个任意类型的 key,可以转换成一个 int 类型的下标

假设此时用 Hash 表记录  id  索引列,我们在每插入一行记录的同时还要维护 Hash 表索引字段。
图片

这时候开始查找  id = 7  的树节点仅找了 1 次,效率非常高了。
图片
但  MySQL  的索引依然不采用能够精准定位的Hash 表。因为它不适用范围查询

结构

哈希表结构,键值通过哈希函数映射到哈希桶。

原理

直接计算哈希值定位数据,无需遍历层级。
Hash算法通过以下步骤将任意长度的输入映射为固定长度的输出(哈希值):

  1. 输入处理:将输入数据(如字符串、文件)视为二进制流。
  2. 分块处理:将输入分成固定长度的块(例如MD5分块为512位)。
  3. 初始化:设置初始哈希值(如MD5初始化为特定的十六进制数)。
  4. 迭代计算:对每个分块进行循环操作,结合当前哈希值与分块数据,通过位运算(如异或、循环移位、逻辑运算)生成新的哈希值。
  5. 最终输出:所有分块处理完成后,输出固定长度的哈希值(如MD5为128位,SHA-256为256位)。

特点

优点

缺点

风险

适用场景

为什么不采用红黑树

红黑树是一种特化的 AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡;
若一棵二叉查找树是红黑树,则它的任一子树必为红黑树。

假设此时用红黑树记录  id  索引列,我们在每插入一行记录的同时还要维护红黑树索引字段。
图片
插入过程中会发现它与普通二叉树不同的是当一棵树的左右子树高度差 > 1 时,它会进行自旋操作,保持树的平衡

这时候开始查找  id = 7  的树节点只找了 3 次,比所谓的普通二叉树还是要更快的。
图片
但  MySQL  的索引依然不采用能够精确定位和范围查询都优秀的红黑树
因为当  MySQL  数据量很大的时候,索引的体积也会很大,可能内存放不下,所以需要从磁盘上进行相关读写,如果树的层级太高,则读写磁盘的次数(I/O交互)就会越多,性能就会越差。

B-tree

红黑树目前的唯一不足点就是树的高度不可控,所以现在我们的切入点就是树的高度
目前一个节点是只分配了一个存储 1 个元素,如果要控制高度,我们就可以把一个节点分配的空间更大一点,让它横向存储多个元素,这个时候高度就可控了。这么个改造过程,就变成了  B-tree 。”

B-tree 是绝对平衡的多路搜索树,每个节点包含多个关键字子节点指针,设计用于高效管理大量数据的读写操作

尤其适合磁盘存储、文件系统、部分数据库索引,例如:MongoDB
图片

关键字(数据索引)

关键字的作用: 关键字在 B 树的内部节点中充当路由(Routing)分隔符(Separators)。它们本身可能不是最终的数据记录,而是用于指引搜索路径

B 树中关键字的工作原理

B 树的核心设计在于,它利用节点内的多个关键字将搜索空间高效地划分成多个子空间。
假设一个 B 树的内部节点包含 k 个关键字 K1,K2,,Kk,并且有 k+1 个子节点指针 P0,P1,,Pk

  1. 关键字的有序排列
    • 节点内的所有关键字都是有序递增排列的:K1<K2<<Kk
  2. 关键字与指针的关联(路由工作)
    • 每个关键字 Ki 将左右两侧的搜索空间分开,它的工作是确定一个目标关键字 X 应该进入哪个子节点(子树):
子节点指针 包含的关键字范围 查找路径决策
P0 X<K1 进入 P0 指向的子节点
Pi (1ik1) KiX<Ki+1 进入 Pi 指向的子节点
Pk XKk 进入 Pk 指向的子节点
  1. “绝对平衡”与“多路搜索”的体现
    • 多路搜索(Multi-way Search):正是因为每个节点包含多个键值,并且能根据这些键值做出 k+1 个选择,所以 B 树被称为多路搜索树,而不是像二叉树那样只有 2 个选择。
    • 绝对平衡(Absolutely Balanced):意味着所有叶子节点(包含数据或数据指针的节点)都位于同一层。这保证了从根节点到任何数据项的查找路径长度(即树高 h)是固定的,因此查找操作的性能非常稳定和高效。

B 树(B-tree)的最小阶 m3

这里的m 指的是一个节点可以拥有的最大子节点数量

最小阶=3

B 树的基本特性要求它必须是一棵多路搜索树(Multi-way Search Tree)并且是平衡的
要满足 B 树的性质,阶 m 必须满足以下两个核心条件:

  1. 节点能分裂和合并: 这是保持树平衡的关键。
  2. 非根内部节点必须有两个及以上的子节点: 否则它就退化成二叉树或更简单的结构。

1. m=2(最大子节点数 <=2)

2. m=3(最大子节点数 <=3)

相关名词

简写 全称 (英文) 全称 (中文) 描述 (作用/含义) 适用节点 约束关系
m Order 一个节点可以拥有的最大子节点数量(最大分支因子)。
m 的大小决定了树的扇出能力。
所有节点 m=2t
(通常定义)
t Minimum Degree / Minimum Branching Factor 最小度 / 最小分支因子 非根内部节点(非根非叶)必须拥有的最小子节点数量
它是定义 B 树平衡性的关键参数。
非根内部节点 t=m2
(向上取整)
k Number of Keys 关键字数 某个节点当前包含的关键字(或索引项)的数量 所有节点 k=d1
d Degree / Number of Children / 子节点数 某个节点当前拥有的子节点(指针)数量
节点的关键字从左到右递增排列
所有节点 d=k+1
tdm
N Number of Records / Total Keys 总记录数 / 总关键字数 整个树或数据库中存储的数据项(或叶子节点中的记录)的总数量 整个树
h Height 高度 从根节点到叶子节点的最长路径上的边数(或节点数,取决于具体定义)。决定了查找操作的最大磁盘 I/O 次数 整个树 hlogmN
L Leaf Capacity 叶子容量 B+ 树叶子节点可以容纳的最大数据记录数(或指向数据记录的指针数)。在某些实现中,L 可能与 m 或 m1 相同。 叶子节点

参数关系

算法的执行时间主要由读/写磁盘的次数决定,一次I/O操作应尽量读写可能多信息,节点规模一般以一个数据页为单位。一个节点包含的关键字及其子节点取决于数据页的大小。

根节点
非根内部节点

优点

缺点

风险

适用场景

名字取义(题外话,放松一下)

以下摘自维基百科
鲁道夫·拜尔(Rudolf Bayer)和 艾华·M·麦克雷(Ed M. McCreight)于1972年在波音研究实验室(Boeing Research Labs)工作时发明了  B-tree ,但是他们没有解释 B 代表什么意义(如果有的话)。

道格拉斯·科默尔(Douglas Comer)解释说:两位作者从来都没解释过  B-tree  的原始意义。我们可能觉得 balanced, broad 或 bushy 可能适合。其他人建议字母 B 代表 Boeing。源自于他的赞助,不过,看起来把  B-tree  当作 Bayer 树更合适些。

高德纳(Donald Knuth)在他1980年5月发表的题为 "CS144C classroom lecture about disk storage and B-trees" 的论文中推测了  B-tree  的名字取义,提出 B 可能意味 Boeing 或者 Bayer 的名字。

查找

B 树查找本质上是二叉树(Binary Search Tree)查找思想的多路泛化

  1. 二叉树节点只有 1 个关键字和 2 个分支,查找时只做左或右的二路决策
  2. 而 B 树节点拥有 k 个关键字和 (k+1) 个分支,查找时通过关键字比较,从多个分支中精确选择一条路径,进行多路决策
    B 树查找的核心是分两级进行
  3. 节点查找(磁盘 I/O)
    • 将目标节点从磁盘读取到内存中。
  4. 关键字查找(内存操作)
    • 在内存中的节点内,使用二分查找(折半查找)定位关键字。
    • 若找到则结束;若未找到,根据关键字大小判断并选择正确的子节点指针,回到第 1 步继续查找。

操作流程

现在需要查找元素:88
第一次:磁盘IO
图片
第二次:磁盘IO
图片
第三次:磁盘IO
然后这有一次内存比对,分别跟 70 与 88 比对,最后找到 88。
图片
B 树的关键优势在于:

  1. 分层计算:B 树将耗时巨大的磁盘 I/O(节点间跳转)与快速的内存比对(节点内查找)清晰分离。
  2. 降低树高:由于 B 树一个节点能存储大量关键字(由阶决定),相同数据量下,它生成的节点总数远少于二叉树。
  3. 核心优势:B 树通过大幅减少节点数量,从而极大地降低了必需的磁盘 I/O 次数,这正是其优于二叉树的性能优势所在。

插入

当  B-tree  要进行插入关键字时,都是直接找到叶子节点进行操作。

  1. 根据要插入的关键字查找到待插入的叶子节点;
  2. 因为一个节点的子节点的最大个数(阶)为 m,所以需要判断当前节点关键字的个数是否小于 (m1)
    • 是:直接插入
    • 否:发生节点分裂,按中间关键字分裂成左右两部分,中间关键字分裂到父节点当做索引存储。

以3阶树举例

叶子节点

插入元素:72

  1. 查找待插入的叶子节点
    图片
  2. 节点分裂
    关键字72插入到[70,88]节点后,后元素数量超过了2个,准备进行分裂;
    中间关键字分裂,中间关键字分裂到父节点当做索引存储。

Tip: 当中间关键字有两个时,通常将左关键字进行上移分裂。
图片

删除

删除操作比查找和插入复杂,原因在于:

  1. 关键字位置不确定:待删除的关键字可能位于内部节点(作为索引)或叶子节点(作为数据)。
  2. 维护平衡:删除后可能导致节点中的关键字数量低于最小限制,必须通过复杂的旋转(借键)合并操作来重新平衡 B 树,以维持其高性能特性。

以5阶树举例
图片

根节点

删除根节点关键字

内部节点

删除非叶子节点的元素后合并+旋转

删除目标:12

  1. 查找元素 12 位置
    图片
  2. 移除 12 后,违背  B-tree  对节点内关键字的要求
    对于非叶子节点元素的删除,我们需要用后继元素覆盖要被删除的元素,然后在后继元素所在的叶子中删除该后继元素。
    图片

叶子节点

直接删除

删除目标:50

  1. 查找元素 50 位置
    图片
  2. [36, 50, 63] 节点移除 50 后,依然符合  B-tree  对节点内关键字的要求:m21km1224
    图片
  3. 删除完成
旋转

eg1:删除目标:11「兄弟节点有元素可借」

  1. 查找元素 11 位置
    图片
  2. [10, 11] 节点移除 11 后,违背  B-tree  对节点内关键字的要求:m21km1214
  3. 需向兄弟节点(左节点优先借,不足则借右节点) 借关键字14进行移动,但不可能让10和14放一起,因为  14 > 12  ,这时候就要进行旋转「将父节点的元素 12 移到该节点,然后 12 就让位给14」
    图片
  4. 删除完毕
    Pasted image 20251024160137
合并+旋转

eg2:接着删除 10「兄弟节点无元素可借」
图片

  1. [10, 12] 节点移除 10 后,违背  B-tree  对节点内关键字的要求
  2. 需要向兄弟节点借元素,但兄弟节点无富余,将父节点的元素 8 移到该节点,这时候 3、6、8、12 都小于14,就先把它们放一起
    Pasted image 20251024160326
  3. 父节点只剩个14,违背了 B-tree 对节点内关键字的要求,将父节点的元素 20 移到该节点,这时候根节点都直接没了,直接合并 14、20、26、72 关键字
    图片
  4. 删除完成
    Pasted image 20251024160509

B+tree

结构

B-tree 的变种,叶子节点包含所有数据并形成有序链表

特点

优点

缺点

风险

适用场景

查找

B+tree  最大的优势就在查找上,主要是范围查询更加明显。

  1. B-tree 节点中的每个关键字都有数据,而 B+tree 中间节点没有数据,只有索引;这就意味着相同大小的数据页可以放更多的节点元素,也就是在相同的数据量下,I/O 操作更少
  2. 在范围查询上,B-tree 需要先找到指定范围内的下限,再找到上限,有了这两个过程后再取出它们之间的元素。B+tree 因为叶子节点通过双向链表进行连接,找到指定范围内的下限后,直接通过链表顺序遍历就行,这样就方便很多了。

在查询单个关键字上,和  B-tree  差不多:先把通过磁盘 I/O 找到节点,再把节点加载到内存中进行内部关键字比对,然后通过大小关系再决定接下来走哪个分支。
但是差别就在于  B+tree  的高度更加可控一些。 MySQL  默认给一个数据页数据分配的大小是 16KB,也就是 16 × 1024 = 16384 字节
官网说明:MySQL :: MySQL 5.7 Reference Manual :: 14.6.2.2 The Physical Structure of an InnoDB Index
证明:直接在数据库中通过  SQL  语句  show GLOBAL STATUS LIKE 'INNODB_page_size' 进行验证

+----------------+-----+
|Variable_name   |Value|
+----------------+-----+
|Innodb_page_size|16384|
+----------------+-----+

当我们的叶子节点全部撑满之后,可以来算一算它树的高度。

我们拿阿里的《Java 开发手册》嵩山版中对表主键的要求进行举例
图片
bigint 64位 占 8Byte,索引旁边放指向下一节点的磁盘文件地址那块是6Byte,是  MySQL  底层写死了的。
图片

通过计算:16384 Byte / (8+6) Byte ≈ 1170,也就是说一个节点设置 16KB 大小的话可以放 1170个索引。
图片
叶子节点一个关键字占用1KB时,那一个节点就可以放16个元素,当整棵树叶子节点全部都被撑满时,通过计算  1170 × 1170 × 16 = 21902400
最后结果为2千多万,树的高度才为3,也就是我们要的高度可控。这也就是为什么  MySQL  的表有上千万数据的情况下,查询效率依然快的原因。

插入

节点内元素数量大于(m1) 时,按中间关键字分裂成左右两部分,中间关键字分裂到父节点当做索引存储,因叶子节点包含所有索引,所以本身它还会分裂右边部分。

以5阶树举例

内部节点

  1. 插入关键字:13
    图片
  2. 关键字13插入到 [9, 10, 11, 12, 13] 节点后元素数量超过了4个,准备进行分裂
    以中间关键字(11)分裂,中间关键字分裂到父节点当做索引存储,本身它也还会分裂右边部分。
    图片
  3. 关键字11 被挪到父节点去之后,节点内元素数量超过了4个,准备进行分裂;
    以中间关键字(7)分裂,中间关键字分裂到父节点当做(冗余)索引存储。
    图片
  4. 插入完成
    Pasted image 20251024162953

叶子节点

  1. 第一次在空树中插入 1
    图片
  2. 再依次插入 2,3,4
    图片
  3. 插入 5
    当插入关键字 5 时,此时节点内元素数量大于 (5-1) ,即超过了4个,这时候就要进行分裂;
    中间关键字分裂,中间关键字分裂到父节点当做索引存储,由于叶子节点包含所有索引数据,所以本身它还会分裂右边部分。
    图片

删除

删除对应节点关键字后,也需要看看节点内剩余关键字是否符合:m21km1

根节点

根节点是唯一一个可以不必满足最小度 t 约束的非叶子节点。
1. 树高度降低

  1. 删除关键字8后,不满足关键字要求:「214
  2. 这时兄弟节点均无富余,就直接与和兄弟节点合并 「合并关键字 [5, 6, 10]」,再删除父节点中的关键字「删除父节点中的关键字 10」
    Pasted image 20251024151625
  3. 这时关键字5的节点,不满足关键字要求:「214
    Pasted image 20251024151648
  4. 这时兄弟节点均无富余,就直接与和兄弟节点合并 「合并关键字 [13, 15]」,此时根节点剩下一个唯一的子节点 [5, 13, 15],合并根节点[11],将子节点 [5, 11, 13, 15] 提升为新的根节点
    Pasted image 20251024153349
  5. 完成删除
    Pasted image 20251024153411
    2. 正常结束
  1. 删除关键字3后,不满足关键字要求:「214
  2. 这时兄弟节点均无富余,就直接与和兄弟节点合并 「合并关键字 [1, 2, 4]」,再删除父节点中的关键字「删除父节点中的关键字 3」

叶子节点

删除操作总是发生在叶子节点。当删除一个记录后,如果叶子节点发生亏损(关键字数 <t1):

1. 借用(旋转)
  1. 删除关键字14后,不满足关键字要求:m21km1214
  2. 直接从兄弟节点(左节点优先借,不足则借右节点) 借关键字进行移动,然后再更新父节点的索引
    图片
  3. 删除完成
    Pasted image 20251023144744
2. 合并
  1. 删除 16 关键字后,它所在的节点只剩 17 一个关键字了,又要准备借关键字;
  2. 这时兄弟节点均无富余,就直接与和兄弟节点合并 「合并关键字 [13, 15, 17]」,再删除父节点中的关键字「删除父节点中的关键字 16」
    图片
  3. 删除完成
    图片

总结

  1. 单个节点存储越多的元素,自然在整个过程中的磁盘I/O交互就越少;
  2. 相对 B-tree 来说,所有的查询最终都会找到叶子节点,这也是 B+tree 性能稳定的一个体现;
  3. 所有叶子节点通过双向链表相连,范围查询非常方便,这也是 B+tree 最明显的优势。

B+tree  vs  B-tree

特性 B 树 (B-tree) B+ 树 (B+tree)
数据存储位置 所有节点(包括内部节点和叶子节点)都可以存储实际数据(数据记录或指向数据的指针)。 实际的数据记录(或指向数据的指针)只存储在叶子节点
包括:关键字、指向数据行的指针(如磁盘地址或数据行的指针)
内部节点作用 既充当索引,又存储部分数据 只充当索引(关键字)和路由(指针)。不存储实际数据
关键字冗余 关键字不冗余。内部节点的关键字不重复出现在叶子节点。 内部节点用作索引的关键字冗余。所有关键字都会出现在叶子节点中。
范围查询效率 效率较低。进行范围查找时,需要对树的每一层都进行遍历,效率不稳定。 效率极高(核心优势)。所有叶子节点构成双向链表,只需定位到范围起点,然后沿链表顺序遍历即可。
查询效率 每次查询,数据可能存储在任何节点,查询性能不稳定(最好情况 1 次 I/O,最差情况 h 次 I/O)。 数据只在叶子节点,任何查找都需要从根到叶子,查询性能稳定(总是 h 次 I/O,其中 h 是树的高度)。
磁盘 I/O 内部节点存储数据,导致节点能容纳的关键字和指针数量较少,树的高度可能更高 内部节点只存储关键字和指针,扇出(Fanout)能力强,节点能容纳更多索引,树的高度更低,减少磁盘 I/O。
进行范围查找时需要获取所有节点,相比之下  B+tree  效率更高。
图片
上图不够完善,更完善的B+tree 底层实际具体的数据结构如下图(每个节点都被称作一个数据页)
image.png

数据页

InnoDB 如何存储数据

虽然数据库记录按存储,但 InnoDB 的 I/O 最小单位是 数据页(默认 16KB)。这意味着:
数据库 I/O 并非以为单位,而是以单位,一次最少读写 16KB 内容到内存或磁盘,以提高效率。

数据页「默认16KB」

组成部分 占用空间 (字节) 存储内容 / 作用
文件头 (File Header) 38 字节 表示页信息的文件头
页头 (Page Header) 56 字节 表示页状态信息的页头
最小、最大记录 (Infimum + Supremum) 26 字节 两个虚拟的伪记录,分别表示页中的最小和最大记录。 行记录
用户记录 (User Records) 不确定 存储实际的行记录内容 行记录
空闲空间 (Free Space) 不确定 页中还未被使用的空闲空间。
页目录 (Page Directory) 不确定 存储用户记录的相对位置,对记录起到索引作用
文件尾 (File Trailer) 8 字节 校验页是否完整
File Header 中有两个指针,分别指向上一个数据页下一个数据页,连接起来的页相当于一个双向的链表,如下图所示:
![image.png 860](https://weichengjun2.dpdns.org/i/2024/11/06/672ad3168b345.png)

页目录 (Page Directory)

  1. 物理与逻辑:数据页采用链表结构实现逻辑连续,无需物理连续存储。
  2. 记录组织页内记录按主键顺序组成单向链表,便于增删。
  3. 检索优化:为提高检索效率,数据页设有页目录,作为记录的索引,实现快速定位,避免全链表扫描。

页目录与记录的关系图

image.png

查找机制

组成部分 作用与机制
记录分组 页内所有记录(不含已删除)被划分为若干组,每组的记录是按主键有序排列的。
页目录 (槽 Slot) 存储每组最后一条记录的地址偏移量(即)。槽相当于分组记录的稀疏索引。
n_owned 字段 存储在每组最后一条记录的头信息中,表示该组拥有的记录条数(上图中粉红色字段)。
查找过程 1. 二分法查找(槽间):利用槽中的最大主键值和槽的有序性,使用二分法快速定位目标记录所在的槽(记录分组)
2. 链表遍历(槽内):定位到槽后,从该槽指向的第一条记录开始,顺序遍历该组内的少量记录,直到找到目标记录。
查找效率保障 为保证查找效率为O(n),InnoDB 限制了每个槽内记录的数量
第一组:1 条记录
中间组:4~8 条记录
最后组:1~8 条记录

查找示例

阶段 机制/目的 关键点
槽间定位 (加速) 使用二分法快速定位目标主键所在的槽(记录分组) 利用槽内最大记录的主键值进行比较,确定搜索范围。
槽内查找 (遍历) 从定位到的槽指向的记录开始,进行顺序链表遍历,直至找到目标记录。 无需从页内最小记录开始遍历,大大减少查找步骤。
页目录查找主键 11 过程(示例)
  1. 第一次二分 (槽间定位):
    • 目标: 查找 04 号槽的中间位。
    • 定位:(0+4)/2=2 号。
    • 判断: 2 号槽最大记录为 8。由于 11>8,搜索范围缩小到 34 号槽。
  2. 第二次二分 (精确定位):
    • 目标: 查找 34 号槽的中间位。
    • 定位:(2+4)/2=3 号(取整,或根据具体算法)。
    • 判断: 3 号槽最大记录为 12。由于 11<12,确定目标记录在 3 号槽内。
  3. 槽内遍历 (顺序查找):
    • 起点: 从 3 号槽指向的记录(主键 9)开始。
    • 查找: 顺序遍历该槽内的记录,向下搜索 2 次,找到主键为 11 的记录。
      核心流程: 两次二分 (槽间) 一次顺序遍历 (槽内)

MySQL 的数据物理存储

MySQL 的数据物理存储方式,尤其是在有索引和无索引时,主要取决于所使用的存储引擎。最常见的两个引擎是 InnoDBMyISAM,它们在这方面有根本的区别。

核心概念:聚簇索引 vs. 非聚簇索引

理解数据物理存储的关键在于理解这两种存储引擎如何组织数据和主键索引。

特性 InnoDB 存储引擎 MyISAM 存储引擎
主键索引结构 聚簇索引 (Clustered Index) 非聚簇索引 (Non-Clustered Index)
数据文件 数据和主键索引存储在同一个文件(.ibd)中,数据行本身就是按主键顺序物理存放的 数据文件.MYD)和索引文件.MYI)是分离的。
数据是按插入顺序存放的。
叶子节点存储 存储完整的数据行记录 存储关键字 (Key)数据记录的物理地址

InnoDB 引擎(主流选择)

InnoDB 采用聚簇索引结构,这是理解其存储的关键。

无索引时(本质上仍有索引)

在 InnoDB 中,一张表必须且只能有一个聚簇索引

有索引时

主键索引 (Primary Key Index / 聚簇索引)
辅助索引 (Secondary Index / 非聚簇索引)

MyISAM 引擎(较少使用)

MyISAM 采用非聚簇索引结构,数据和索引是完全分离的。

无索引时

有索引时

主键索引 (Primary Key Index / 非聚簇索引)
辅助索引 (Secondary Index / 非聚簇索引)

叶子节点的双向链表是逻辑上连续的

MySQL InnoDB 存储引擎中,B+ 树叶子节点之间连接的双向链表是逻辑上连续的,但在物理上通常是不连续的。

逻辑上的双向链表

物理上的不连续性

小结

总结

特性 InnoDB MyISAM
数据文件 .ibd(数据和索引在一起) .MYD(数据) + .MYI(索引)
数据物理顺序 严格按照主键排序(聚簇) 按插入顺序(非聚簇)
主键索引叶子 存储整行数据 存储数据物理地址
辅助索引叶子 存储主键值(需回表) 存储数据物理地址(无需回表)
无主键时 自动生成隐式 ID 作为聚簇索引 只是没有索引,数据依旧按插入顺序存放

B+ 树的查询

InnoDB 的索引策略总结

问题场景 解决方案 目标与效果
页内查找效率 页目录(槽)机制 将页内记录分组,通过槽号实现二分查找,将页内查找时间复杂度降到最低。
大规模数据定位 B+ 树索引 解决海量记录跨页存储时的快速定位问题。
性能优化 “矮胖” B+ 树 最大化 B+ 树的扇出能力,使树的高度 h 尽可能低,从而最小化磁盘 I/O 次数,保证查询效率。
核心: 页目录 负责页内加速,B+ 树 负责跨页定位,两者共同确保了 InnoDB 的高效检索。

InnoDB 里的 B+ 树中的每个节点都是一个数据页,结构示意图如下:
image.png
通过上图,我们看出  B+ 树的特点:

聚簇/非聚簇索引

主键索引(聚簇索引)

二级索引(非聚簇索引)

叶子节点内容

存储引擎 索引类型 叶子节点内容 核心作用
InnoDB 主键索引 索引键值+完整数据行(所有字段)+相邻叶子节点的指针 数据即索引,直接存储数据,减少 I/O,支持高效范围查询。
InnoDB 二级索引 索引键值 + 主键值+相邻叶子节点的指针 通过主键回表获取完整数据,避免重复存储数据。
MyISAM 主键/二级索引 索引键值 + 行号(数据地址) 直接定位数据文件中的物理位置,索引与数据分离,查询时无需回表。

InnoDB 与 MyISAM 的对比

对比项 InnoDB MyISAM
数据存储 数据与主键索引存储在同一棵 B+ 树(聚簇索引,文件.ibd中) 数据(.MYD)与索引(.MYI)分开存储
叶子节点内容 聚簇索引存完整数据,二级索引存主键值 索引叶子节点存键值 + 行号(数据地址)
查询流程 主键查询一次 I/O,二级索引需两次 I/O(回表) 所有查询均需通过索引直接定位数据,无需回表
适用场景 事务、高并发、范围查询 读多写少、简单查询

实际场景举例

  1. InnoDB 聚簇索引查询
SELECT * FROM user WHERE id = 5;  -- 一次 I/O,直接通过主键索引找到数据。
  1. InnoDB 非聚簇索引查询
SELECT * FROM user WHERE name = '张三';  
-- 两次 I/O:1. 通过 `name` 索引找到主键 `id=5`;2. 通过主键查数据。
  1. MyISAM 索引查询
SELECT * FROM user WHERE age = 25;  
-- 一次 I/O:通过 `age` 索引直接找到行号,定位到数据文件。

设计原则

总结

  1. I/O 基本单位:InnoDB 以 数据页(默认 16KB)为最小 I/O 单位。
  2. 页间组织:数据页间通过双向链表实现逻辑连续,物理可不连续。
  3. 页内优化:页内记录按主键顺序由单向链表连接;通过页目录(槽/分组)和二分查找实现页内记录的高效检索。
  4. 页间索引(B+ 树):采用 B+ 树作为索引,每个节点对应一个数据页,用于高效定位数据页。
  5. 索引分类
    • 聚簇索引:B+ 树叶子节点存储实际数据,每表只有一个
    • 二级索引:B+ 树叶子节点存储主键值,每表可有多个
  6. 二级索引查找优化
    • 索引覆盖:所需数据全部在二级索引中找到,避免回表。
    • 回表:二级索引查到主键后,需通过聚簇索引二次查找获取完整数据行。