云计算百科
云计算领域专业知识百科平台

【MySQL篇|第八篇】索引底层实现

在日常开发中,只要表数据量稍微大一点,慢查询就会成为绕不开的问题。而解决慢查询最直接、最有效的手段,往往就是加索引。但很多同学对索引的理解停留在“加了就快”的层面,面试时被追问“索引底层是怎么实现的”就容易卡壳。本文从索引的本质出发,一步步拆解 MySQL 索引的底层数据结构,重点讲清楚 B+ 树为什么能成为 InnoDB 的默认选择。

一、索引到底是什么

从最朴素的角度理解,索引就是一种帮助 MySQL 高效获取数据的数据结构。它的作用类似于书籍的目录:如果没有目录,想找某一章内容只能从第一页翻到最后一页;有了目录,就能直接定位到目标页码。所以这个目录的设置是非常关键的,我们的目录有很多种做法,有些的目录清晰明了,而有些的很难很快的定位到需要的地方,因此这个目录的制作也是一个关键部分。

把这句话拆开看,有三个关键点:

  • 本质是数据结构:索引不是“魔法”,它本身也要占用磁盘空间,也需要维护。
  • 目的是高效获取数据:索引把“全表扫描”的 O(n) 查找,优化成近似 O(log n) 的查找。
  • 代价是空间和维护成本:索引越多,写入时需要的额外更新越多,占用的磁盘也越大。

在 InnoDB 中,索引和数据是存放在一起的,索引即数据,数据即索引。这一点非常关键,后面讲聚簇索引时会反复用到。

二、为什么不用二叉树、红黑树或哈希

既然索引是数据结构,那为什么 MySQL 不直接用最熟悉的二叉树?原因在于磁盘 IO 的特性。

2.1 二叉搜索树的退化问题

普通二叉搜索树在数据有序插入时会退化成链表,查找复杂度从 O(log n) 退化为 O(n)。即使换成平衡二叉树(AVL)或红黑树,树的高度依然偏高。假设一张表有一千万条数据,红黑树的树高大约在 20 到 30 层,每下降一层就可能意味着一次磁盘 IO,这个代价是无法接受的。因此这个方法还是行不通,只能适用于那些数据量少的一些表。

2.2 哈希索引的局限

哈希索引的等值查询是 O(1),看起来很美,但它有两个致命问题:

  • 不支持范围查询,因为哈希值本身没有顺序性。
  • 不支持排序,也无法利用最左前缀原则。

所以哈希索引只适合等值查询场景,InnoDB 的自适应哈希索引也是内部优化,用户无法直接创建。

2.3 磁盘 IO 才是真正的瓶颈

数据库的数据存放在磁盘上,磁盘读取以页(Page)为单位,InnoDB 默认一页 16KB。一次磁盘 IO 会把整页数据读入内存。因此,索引结构设计的核心目标不是“比较次数最少”,而是让树的高度尽可能低,从而减少磁盘 IO 次数。这就引出了多路平衡查找树。

三、B 树与 B+ 树的结构对比

3.1 B 树的基本形态

B 树是一种多路平衡查找树,一个节点可以存放多个 key 和多个子节点指针。相比二叉树,B 树把“瘦高”的形态变成了“矮胖”,树高大幅降低。但 B 树的非叶子节点也存储数据,这会带来两个问题:

  • 每个节点能容纳的 key 数量变少,树高相对更高。
  • 范围查询需要在树中反复中序遍历,效率不够稳定。

3.2 B+ 树的改进

B+ 树在 B 树基础上做了三点关键改进:

  • 非叶子节点只存索引键,不存数据,因此一个 16KB 的页能放下更多的键,树高更低。
  • 所有数据都存放在叶子节点,查询路径长度一致,性能稳定。
  • 叶子节点之间用双向链表连接,范围查询只需顺着链表扫描即可。
  • 下面用一张简化的结构图说明 B+ 树的形态:

    flowchart TD
    A["根节点: 15 | 30"] –> B["内部节点: 5 | 10"]
    A –> C["内部节点: 20 | 25"]
    A –> D["内部节点: 35 | 40"]
    B –> E["叶子: 1,3,5"]
    B –> F["叶子: 7,9,10"]
    C –> G["叶子: 16,18,20"]
    C –> H["叶子: 22,24,25"]
    D –> I["叶子: 32,34,35"]
    D –> J["叶子: 38,40,42"]
    E -.双向链表.-> F
    F -.双向链表.-> G
    G -.双向链表.-> H
    H -.双向链表.-> I
    I -.双向链表.-> J

    可以看到,内部节点只负责导航,真正的数据全部在叶子层,并且叶子层是一条有序链表。这正是 B+ 树能同时兼顾等值查询和范围查询的原因。

    四、InnoDB 的索引实现:聚簇索引与二级索引

    4.1 聚簇索引

    InnoDB 使用主键构建聚簇索引(Clustered Index),叶子节点存放的是整行数据。也就是说,聚簇索引的叶子节点就是数据页本身。如果没有显式定义主键,InnoDB 会选择一个唯一非空索引代替,再没有就生成一个隐藏的 6 字节 row_id。

    这也解释了一个常见现象:按主键查询最快,因为一次 B+ 树查找就能拿到整行数据。

    4.2 二级索引与回表

    除了主键索引,我们创建的其他索引都叫二级索引(Secondary Index)或辅助索引。二级索引的叶子节点存放的是索引列的值 + 主键值,而不是整行数据。

    因此,通过二级索引查询非索引列时,需要先查到主键,再拿主键去聚簇索引里查一次完整数据,这个过程叫回表。回表意味着两次 B+ 树查找,这也是为什么“尽量使用覆盖索引”会成为优化常识。

    4.3 覆盖索引

    如果查询需要的列全部包含在二级索引中,就无需回表,这种索引称为覆盖索引。例如:

    — 假设存在联合索引 idx_name_age (name, age)
    SELECT name, age FROM user WHERE name = 'Tom';

    这条查询只需要扫描 idx_name_age 就能拿到全部结果,执行计划中 Extra 会显示 Using index。

    五、索引的物理存储与页结构

    InnoDB 以页为单位管理数据,索引页的结构大致如下:

    区域作用
    File Header 记录页的类型、页号、上下页指针等信息
    Page Header 记录页内记录数、空闲空间位置、最大事务 ID 等
    Infimum + Supremum 页内虚拟的最小和最大记录,作为链表边界
    User Records 实际存储的行记录,按主键顺序组成单向链表
    Page Directory 页内记录的稀疏目录,用于页内二分查找
    File Trailer 校验页是否完整,配合刷盘保证一致性

    页内记录通过单向链表串联,Page Directory 提供稀疏索引,使得页内查找也能用二分法。页与页之间则通过 File Header 中的前后指针形成双向链表,这就是 B+ 树叶子层链表的物理基础。

    六、联合索引与最左前缀原则

    联合索引是把多个列组合起来建一棵 B+ 树,排序规则是先按第一列排序,第一列相同再按第二列排序,以此类推。这直接决定了最左前缀原则:

    — 联合索引 idx_abc (a, b, c)
    SELECT * FROM t WHERE a = 1 AND b = 2; — 命中 a、b
    SELECT * FROM t WHERE b = 2 AND c = 3; — 无法使用索引
    SELECT * FROM t WHERE a = 1 AND c = 3; — 只命中 a
    SELECT * FROM t WHERE a = 1 AND b > 2 AND c = 3; — c 无法用于索引查找

    理解最左前缀的关键,是把它想象成一本先按姓氏、再按名字排序的电话簿:你只知道名字而不知道姓氏,是无法快速定位的。

    七、索引失效的常见场景

    建了索引不代表一定用得上,以下场景容易导致索引失效:

    这个我在前面具体讲到过,不了解的可以去看前几期文章

    • 对索引列做函数运算或表达式计算,如 WHERE YEAR(create_time) = 2024。
    • 隐式类型转换,如字符串列用数字比较 WHERE phone = 13800000000。
    • 以通配符开头的模糊查询,如 LIKE '%abc'。
    • 使用 OR 且两侧列没有都建索引。
    • 违反最左前缀原则的联合索引查询。
    • 优化器判断全表扫描更快,例如区分度极低的列。

    排查时建议使用 EXPLAIN 查看 type、key、rows 和 Extra 字段,确认索引是否真正生效。

    八、索引设计的一些实践建议

  • 优先使用自增主键,避免随机主键导致页分裂和碎片。
  • 控制索引数量,一般单表索引不超过 5 个,写多读少的表尤其要克制。
  • 尽量使用覆盖索引,减少回表带来的额外 IO。
  • 联合索引把区分度高的列放前面,同时兼顾最左前缀的查询习惯。
  • 避免在索引列上做运算,把计算转移到常量侧。
  • 定期关注慢查询日志,用真实数据验证索引效果,而不是凭感觉建索引。
  • 九、总结

    回到最初的问题:MySQL 索引是什么?它是一种为了减少磁盘 IO 而设计的有序数据结构;底层怎么实现?InnoDB 用 B+ 树组织索引,非叶子节点只存键做导航,叶子节点存数据并用双向链表串联,从而同时支持高效的等值查询和范围查询。聚簇索引的叶子节点是整行数据,二级索引的叶子节点是索引列加主键,需要时通过回表补齐数据。

    理解了这层结构,很多看似零散的优化规则——覆盖索引、最左前缀、避免函数运算、慎用随机主键——都会变得顺理成章,因为它们本质上都是在减少树的高度、减少回表次数、减少磁盘 IO。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【MySQL篇|第八篇】索引底层实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!