很多人都有过这种困惑:明明给表建了联合索引,查询却没走;明明单列索引都能命中,联合索引反而失效;为什么 MySQL 官方的建议总是“把最左列放在第一位”?这些疑问的背后,都指向同一个问题——主键索引和联合索引在 InnoDB 里到底是怎么存的。
这篇文章,我会把这两类索引的底层存储结构完整拆开讲清楚。内容包括 B+ 树的组织方式、聚簇索引与二级索引的差异、联合索引叶子节点里的排序规则、最左前缀原则的物理由来,以及回表、覆盖索引、索引下推这些进阶机制的实现逻辑。同时会补充我在设计索引时踩过的坑和总结的判断方法,适合已经会写 SQL、想深入理解 MySQL 运行原理的开发者和 DBA 阅读。
1. 索引为什么快:从一次磁盘读取说起
1.1 全表扫描的困境
在讲索引原理之前,得先搞清楚“没有索引时数据库在做什么”。假设有一张订单表,里面有 100 万行数据,现在你要查 user_id = 10086 的订单。InnoDB 并不知道数据放在哪个页里,只能从表空间的第一个数据页开始,一页一页往下读,逐行比对 user_id。这个操作叫全表扫描。
表面看只是“逐行比对”,但真正的开销在磁盘 I/O。InnoDB 的最小读写单位是页,默认 16KB。也就是说,哪怕你只要一行数据,也得先把这个行所在的整个 16KB 数据页从磁盘加载到内存。100 万行数据大概占用几万到十几万个页,全表扫描意味着要把这些页基本都读一遍。机械硬盘的随机读取延迟大约是 5 到 10 毫秒,即便用 SSD,也要几十到上百微秒。乘上几万个页,这个等待时间就非常可观了。
所以索引的本质,就是想办法减少需要读取的数据页数量。而要做到这一点,前提是“数据按某种可预测的顺序组织”,让查询可以直接跳到目标位置附近,而不是从头开始翻。
1.2 B+ 树:为了减少磁盘 I/O 而生的多路搜索树
MySQL 的 InnoDB 引擎为什么选 B+ 树作为索引结构,而不是二叉树、红黑树或者哈希表?核心原因有三点。
第一,B+ 树是多叉树,能有效控制树高。一个节点可以存储成百上千个键值,对于 100 万行的表,B+ 树的高度通常只有 3 到 4 层。这就意味着定位一条记录,最多只需要 3 到 4 次磁盘 I/O,因为每一层的节点都对应一次页读取。而二叉树在数据量大的时候,树高会达到几十层,每次查询都要做几十次 I/O,性能完全不可接受。
第二,B+ 树的所有数据都存放在叶子节点,并且叶子节点之间用链表连接。这个设计对范围查询极其友好。MySQL 里最常见的查询除了等值命中,还有 BETWEEN、>、< 这一类范围条件。B+ 树通过叶子节点的有序链表,可以顺着链表顺序扫描,一次性读出所有符合条件的记录,而不需要回到父节点重新寻找。这在关系型数据库的场景里是致命的优势,也是哈希索引做不到的——哈希索引能用 O(1) 时间找到单个等值,但无法支持任何范围扫描。
第三,B+ 树的内节点只保存键值和指针,不保存数据。这意味着同一个 16KB 的页里能容纳更多键值,从而让整棵树变得更矮更宽,进一步减少磁盘 I/O 次数。
提示:理解索引原理时,始终把“页/磁盘 I/O”放在脑子里。索引优化的所有内容,本质上都是在用“空间换 I/O 次数”。
1.3 为什么不是哈希索引或者平衡二叉树
有一个常见的误区:既然哈希索引查找等值最快,为什么 MySQL 不用哈希做主要索引?因为业务查询几乎不会只有点查。一个正常的表,总有范围查询、排序、分组这类需求,哈希结构完全无法处理。所以 InnoDB 只在自适应哈希索引里用哈希结构,做热点数据的内存加速,而不是替代 B+ 树。
平衡二叉树的问题更明显。它的每个节点只存一个键值,节点分叉只有两个方向,数据量一大树高就非常高。而且为了保证平衡,插入删除时的旋转操作代价很大。红黑树虽然有自平衡能力,但树高仍然远高于 B+ 树,并且数据分散在各个节点上,范围查询需要频繁回溯父节点,磁盘 I/O 次数显著增加。
所以,B+ 树能在数据库索引领域胜出,不是我个人的偏好,而是磁盘存储这个硬件前提下的必然选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
