【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 树基础上做了三点关键改进:

  1. 非叶子节点只存索引键,不存数据,因此一个 16KB 的页能放下更多的键,树高更低。
  2. 所有数据都存放在叶子节点,查询路径长度一致,性能稳定。
  3. 叶子节点之间用双向链表连接,范围查询只需顺着链表扫描即可。

下面用一张简化的结构图说明 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 覆盖索引

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

sql 复制代码
-- 假设存在联合索引 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+ 树,排序规则是先按第一列排序,第一列相同再按第二列排序,以此类推。这直接决定了最左前缀原则:

sql 复制代码
-- 联合索引 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 字段,确认索引是否真正生效。

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

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

九、总结

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

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

相关推荐
updayday8541 小时前
电脑换了硬盘、加了内存,资产台账却没更新 - Ping64 硬件资产管理
大数据·网络·数据库·安全·智能路由器
hhhjhl2 小时前
mime-types 跨端三方库仓颉适配全流程
数据库
广州智造2 小时前
Radioss中破坏模型FAILTAB1的详细介绍
数据库·人工智能·算法·机器学习·仿真软件·radioss
李兆龙的博客2 小时前
从一到无穷大 #101:SSD 的工作原理——从 NAND 到数据库读写
数据库·ssd·存储
吹什么轩2 小时前
MySQL 内置函数全景:日期 / 字符串 / 数学 / 其他,一次讲透(附索引杀手排查)
java·数据库·面试
CS创新实验室2 小时前
改了模型,数据库为什么能自动跟上?——Django 迁移(migrations)的底层
数据库·django·sqlite
编码追梦人2 小时前
基于 ESP32-S3 的便携式多参数环境监护仪设计:从硬件选型到 FreeRTOS 多任务落地
数据库·单片机·嵌入式硬件·mongodb·esp32·freertos·低功耗设计
sunburn-2 小时前
数据库增删改查:从 SQL 基础到实践要点
java·数据库·mysql
誰能久伴不乏4 小时前
桌面客户端的运行日志系统:SQLite 分块 + 独立写线程 + 文本降级
数据库·c++·qt·架构·sqlite