Skip to content

核心结论

简单来说,InnoDB选择B+树作为其主要的索引结构,是因为B+树在“范围查询”、“I/O效率”和“查询稳定性”这几个方面,相较于B树和哈希表,更能满足关系型数据库(尤其是磁盘存储)的核心需求。


1. 为什么不用哈希表 (Hash Table)?

哈希表是一种通过哈希函数将键(Key)映射到值(Value)存储位置的数据结构。

  • 优点:

    • 等值查询极快: 在理想情况下,如果哈希冲突不严重,通过键进行精确查找(如 WHERE id = 100)的时间复杂度是 O(1),速度非常快。
  • 缺点 (不适用于InnoDB主索引的原因):

    • 不支持范围查询: 哈希表的存储是无序的。哈希函数计算出的值是离散的,你无法高效地进行范围查找(如 WHERE age > 20WHERE name LIKE '张%')。要完成这样的查询,哈希表几乎需要扫描所有数据,这效率极低。而范围查询在数据库中是极为常见的操作。
    • 哈希冲突问题: 当不同的键计算出相同的哈希值时,就会产生哈希冲突。解决冲突会带来额外的开销(如链地址法、开放地址法等),在数据量大、冲突严重时,性能会急剧下降。
    • 不适合组合索引: 在多列组成的联合索引中,哈希索引无法利用索引的部分前缀进行查询。

注意: MySQL也支持哈希索引,但通常是内存存储引擎(如MEMORY引擎)的默认选择。InnoDB也提供了一种“自适应哈希索引”(Adaptive Hash Index),但这只是在B+树索引之上的一种优化,用于对热点数据页的频繁等值查询进行加速,其底层基础仍然是B+树。


2. 为什么不用B树 (B-Tree),而用其变体B+树?

B树和B+树非常相似,都是多叉平衡搜索树,特别适合于磁盘等外部存储设备。它们都通过降低树的高度来减少磁盘I/O次数。但是B+树是B树的优化和增强版本,更适合数据库索引。

让我们看看B树和B+树的关键区别:

特性B树 (B-Tree)B+树 (B+Tree)
数据存储位置所有节点(内部节点和叶子节点)都存储数据。只有叶子节点存储完整的数据行。内部节点只存储键和指向下一层节点的指针。
叶子节点连接叶子节点之间没有指针相连。所有叶子节点通过一个双向链表连接,形成一个有序序列。
查询方式查找可能在任何节点结束。所有查找最终都必须到达叶子节点才能获取数据。

基于这些区别,B+树的优势就体现出来了:

  • 1. 更高的I/O效率(单次查询):

    • 由于B+树的内部节点不存储数据(只存键和指针),所以在同样大小的磁盘页(Page,InnoDB默认为16KB)中,B+树的内部节点可以容纳比B树更多的键。
    • 这意味着B+树的“扇出”(一个节点能拥有的子节点数量)更大,树的高度会更低。对于同样数据量的索引,B+树通常比B树更“矮胖”。
    • 查询时,从根节点到叶子节点的路径更短,需要进行的磁盘I/O次数就更少,查询效率更高。
  • 2. 更适合范围查询和全表扫描:

    • 这是B+树最关键的优势。由于所有数据都存储在叶子节点,并且叶子节点之间通过双向链表连接,形成了一个有序链表。
    • 当进行范围查询(如 WHERE id BETWEEN 100 AND 200)时,B+树只需要定位到起始的叶子节点(id=100),然后通过链表顺序遍历后续的叶子节点,直到范围结束。这个过程是线性的,非常高效。
    • 而B树要完成同样的范围查询,需要进行复杂的中序遍历,可能需要频繁地在树的层级之间来回跳转,I/O成本远高于B+树。
    • 对于全表扫描 (SELECT * FROM table;),B+树可以直接遍历叶子节点的链表即可,而B树则需要遍历整棵树。
  • 3. 更稳定的查询性能:

    • 由于所有数据查询最终都必须落到叶子节点,所以任何一个键的查询路径长度都是相同的(都等于树的高度)。这使得查询性能非常稳定。
    • 而B树的查询可能在内部节点就结束了(如果数据恰好在内部节点),也可能在叶子节点结束,导致查询性能不稳定。

总结

数据结构优点缺点为什么InnoDB不用/少用
哈希表等值查询快 (O(1))不支持范围查询;哈希冲突影响性能SQL中范围查询 (><BETWEENLIKE) 非常普遍,这是其致命弱点。
B树查询效率高,支持范围查询内部节点存储数据导致扇出小、树高;范围查询效率不如B+树在I/O效率和范围查询性能上,其增强版B+树表现更出色。
B+树I/O效率高(树更矮胖);范围查询极快(叶子节点链表);查询性能稳定结构相对B树更复杂一些完美契合关系型数据库在磁盘上的索引需求,平衡了单点查询和范围查询的性能。

因此,InnoDB存储引擎综合考虑了查询类型、磁盘I/O效率以及整体性能稳定性,最终选择了B+树作为其核心的索引数据结构。这是在数据库应用场景下做出的最优权衡。

好的,我们来把这个关键点——“更高的I/O效率”——用一个更具体、更形象的方式来拆解和深化一下。

核心前提:磁盘I/O是数据库最慢的操作

首先,我们要牢记一个大前提:在数据库中,最昂贵、最耗时的操作是从磁盘读取数据到内存。内存的读写速度是纳秒级别的,而磁盘的读写速度是毫秒级别的,两者相差数万倍甚至更多。

数据库索引文件是存储在磁盘上的。当我们通过索引查找数据时,每读取一个树的节点(Node),通常就对应着一次磁盘I/O操作。因此,数据库索引设计的核心目标之一,就是尽可能地减少磁盘I/O的次数

而减少I/O次数最直接的方法,就是降低树的高度

拆解解释:为什么B+树比B树更“矮胖”?

让我们用一个具体的例子来说明。

基本设定:

  • 数据库页(Page)大小: 假设一个数据页的大小固定为 16KB。这是InnoDB的默认值,也是一次I/O操作的基本单位。
  • 索引键类型: 假设我们索引的是一个BIGINT类型的主键,占用 8 字节。
  • 指针大小: 假设在B树/B+树中,指向另一个数据页的指针占用 6 字节。

场景一:B树的内部节点

B树的特点是:所有节点(包括内部节点和叶子节点)都存储数据。 一个B树的内部节点,其结构大致是: [ (指针), (键1, 数据1), (指针), (键2, 数据2), ... ]

  • 空间占用分析:

    • 假设每条“数据”部分非常小,仅仅是一个指向真实数据行的指针,也需要比如 8 字节。
    • 那么,存储一个键值对和它旁边的指针,需要的空间是:指针(6字节) + 键(8字节) + 数据(8字节) = 22字节
  • 计算扇出 (Fan-out):

    • 一个 16KB 的页能容纳多少个这样的单元?
    • 16 * 1024 / 22 ≈ 744
    • 这意味着,一个B树的内部节点,最多能指向约 744 个子节点。这个744就是它的“扇出”。
    • (注意:这还是在“数据”部分极小的情况下的乐观估计。如果数据直接存在节点里,比如一个几十字节的字符串,那么扇出会急剧减小。)

场景二:B+树的内部节点

B+树的特点是:内部节点只存储“键”和“指针”,不存储任何数据。 一个B+树的内部节点,其结构大致是: [ (指针1), (键1), (指针2), (键2), ... ]

  • 空间占用分析:

    • 存储一个键和指向下一层节点的指针,需要的空间是:键(8字节) + 指针(6字节) = 14字节
  • 计算扇出 (Fan-out):

    • 一个 16KB 的页能容纳多少个这样的单元?
    • 16 * 1024 / 14 ≈ 1170
    • 这意味着,一个B+树的内部节点,最多能指向约 1170 个子节点。它的扇出是1170。

对比与结论:高度决定I/O

现在我们来比较一下,假设我们要索引 1亿 条数据:

  • B+树 (扇出 1170):

    • 高度为2的树1170 * 1170 ≈ 136万 (根节点 -> 叶子节点)。可以索引136万条数据。
    • 高度为3的树1170 * 1170 * 1170 ≈ 16亿
    • 结论:索引1亿条数据,B+树的高度最多为3。这意味着从根节点查到任何一条数据所在的叶子节点,只需要 3次 磁盘I/O。
  • B树 (扇出 744):

    • 高度为2的树744 * 744 ≈ 55万
    • 高度为3的树744 * 744 * 744 ≈ 4.1亿
    • 结论:索引1亿条数据,B树的高度需要3
  • 如果数据更大一点呢?

    • 在我们的例子中,如果数据行本身比较大,导致B树内部节点能存储的单元更少,扇出降到比如100。
    • B树 (扇出 100):
      • 高度为2:100 * 100 = 1万
      • 高度为3:100 * 100 * 100 = 100万
      • 高度为4:100 * 100 * 100 * 100 = 1亿
      • 这时,索引1亿条数据,B树的高度就需要4,需要 4次 磁盘I/O。

而B+树的内部节点扇出基本不受数据行大小的影响,依然是 3次 I/O。

总结

通过这个对比,我们可以清晰地看到:

  1. 根本原因:B+树的内部节点不存储数据,使其变得“轻量”。
  2. 直接结果:在同一个磁盘页(Page)中,B+树的内部节点可以塞下更多的“路标”(键和指针),从而拥有更大的“扇出”(Fan-out)。
  3. 最终优势:更大的扇出使得B+树的整体高度比B树更低。对于海量数据,一颗高度为3的B+树足以索引上亿甚至几十亿的数据。树的高度每降低1,查找数据就减少1次磁盘I/O。

所以,“更高的I/O效率”指的就是,通过增加扇出、降低树高这种结构上的优化,B+树在查找数据时,需要访问磁盘的次数更少,从而让单次查询的速度更快。这就是它在I/O效率上优于B树的核心原因。

Released under the MIT License.