直接以博文正文开始:
平时跟同事聊 MySQL 索引,聊得最多的就是“这条 SQL 为什么没走索引”“那个表加了联合索引为什么查询还这么慢”。很多人背过八股文,知道主键索引和联合索引的存储结构,但一落到真实业务和 SQL 优化上就开始凭感觉操作。这篇文章我不打算抄书,而是把主键索引和联合索引的原理掰开来讲:它们底层到底怎么存数据、回表是怎么回事、联合索引最左前缀原则背后的原因是什么,以及实战中最容易踩的坑和排查方法。适合正在学习 MySQL 底层机制的人,也适合写了好几年 SQL 但从没系统梳理过索引原理的开发同学。
我默认你用的是 InnoDB 引擎,这是 MySQL 5.5 之后默认的存储引擎,也是绝大多数业务系统的选择。搞清楚 InnoDB 里的索引模型,比死记索引规则有用得多。
1. 先搞懂 InnoDB 的索引模型:B+ Tree 是怎么存储数据的
1.1 为什么索引选择了 B+ Tree,而不是二叉查找树或哈希表
很多人一开始会想,查找最快的不就是哈希表吗?O(1) 的复杂度,为什么 MySQL 不直接用哈希索引做主索引?这个问题想明白了,B+ Tree 的设计意图也就清楚了一大半。
哈希索引确实能单条等值查询做到接近 O(1),但它解决不了范围查询和排序。比如 WHERE id > 100 AND id < 1000,哈希结构只能全表遍历,因为哈希值的排列顺序和数据本身没有单调关系。而 B+ Tree 是排好序的多叉平衡树,叶子节点之间通过双向指针串联,既能走等值查询,也能高效走范围扫描。
二叉查找树同理,理论上每个节点的查找是 O(logN),但树的高度受数据量影响。当数据量达到千万级时,二叉树的高度动辄二十几层,而 InnoDB 的页大小是 16KB,一个节点能放下成百上千个键值。MySQL 使用 B+ Tree 一个核心考量就是把树高控制在 2 到 4 层,极端情况下三层 B+ Tree 就能支撑千万级数据,意味着查询最多只需要几次磁盘 I/O。
1.2 聚簇索引:主键就是整行数据存在的位置
InnoDB 中,主键索引也叫聚簇索引。聚簇这个词听起来抽象,但你记住一句话就够用了:聚簇索引的叶子节点存的是完整的一整行记录。
也就是说,表里的数据行并不是独立存放在某个文件里,而是直接按照主键的顺序组织在 B+ Tree 的叶子节点上。主键是 1、2、3,数据在磁盘上物理排布也基本按 1、2、3 的顺序。这个设计带来一个特点:按主键查数据,只要从 B+ Tree 根节点往下走到叶子节点,读到的就是目标行的完整字段,不需要再做任何二次查询。
这里有个自然的推论——InnoDB 表必须有主键。如果你建表时没有显式指定主键,InnoDB 会先找第一个非空的唯一索引作为主键;如果连唯一索引都没有,它就会自动生成一个 6 字节的隐式主键 ROWID,但这个 ROWID 对应用层完全透明,你也无法利用它做查询优化。所以建表时务必要指定主键,否则你可能白白付出额外的存储和 I/O 代价。
还有一个很多人在意的概念叫索引组织表。因为数据按照主键排序存储,插入新记录时 InnoDB 会找到主键排序后的正确位置,再插入。如果主键值是单调递增的,那么新记录永远追加在末尾,代价最小。如果主键值是随机的,插入时可能会造成页分裂和页重排,产生大量随机 I/O 和碎片,写入性能明显下降。这也是为什么业界普遍推荐用自增主键或雪花算法生成的趋势递增主键,而不是直接用 UUID 字符串。
1.3 非聚簇索引的落叶归根指向主键
除了主键索引之外,其他索引都叫二级索引,也叫非聚簇索引。你手动加的普通索引、唯一索引,以及下面要讲的联合索引,都属于二级索引。
二级索引的 B+ Tree 结构和主键索引大致相同,但有一个关键差异:二级索引叶子节点存放的不是完整行数据,而是当前索引列的值加上对应的主键值。
举个例子,假设你在一张 user 表的 name 字段上建了一个普通索引,那么这颗二级索引树的结构大致是:每个叶子节点存了 (name, id) 这样的二元组,并按 name 排序。当你执行 SELECT * FROM user WHERE name = '张三' 时,MySQL 先在二级索引树上查到 name 等于张山的记录,发现叶子节点里只有 name 和主键 id,并没有你要的完整字段,于是拿到主键 id 再去主键索引树查一次,把整行数据读出来。这个“拿到主键再回主键索引查询”的过程就叫回表。
回表本身不是 bug,是 InnoDB 为了节省存储空间做出的均衡设计。如果每个二级索引叶子节点都存一份完整行数据,那每建一个索引就等于把整张表复制一遍,磁盘占用和写入开销将无法接受。而只存主键值,既能让索引体积小,也能保证所有二级索引都能通过主键关联回完整数据。
理解这个模型后,你自然就明白为什么 InnoDB 强烈建议主键越短越好。因为主键值会被复制到每一个二级索引的叶子节点里,主键越长,所有二级索引的体积就越大,占用的内存和磁盘也就越多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 联合索引的本质:排序规则与最左前缀的真正逻辑
2.1 联合索引的内部结构是一棵单树,而不是多个独立索引
很多初学者会把联合索引理解成“在多个列上分别建索引”,这是最大的误区。ALTER TABLE user ADD INDEX idx_name_age (name, age) 建立的是一个联合索引,而不是 name 上的索引加上 age 上的索引。它底层只用一棵 B+ Tree,这棵树先按第一个字段 name 排序,在 name 相同的情况下再按第二个字段 age 排序,如果有第三个字段,就接着按第三个字段排序。
你可以类比成查英文字典的过程。字典先按首字母排序,首字母相同再看第二个字母,以此类推。你查一个词的时候,如果你只知道第三个字母,是无法通过字典的目录结构减少查找范围的,只能从头翻到尾。
联合索引内部数据排列大概是这样,我用极简示例表示一下:
- ('Alice', 21, id=1)
- ('Alice', 25, id=6)
- ('Bob', 20, id=3)
- ('Bob', 24, id=8)
- ('Bob', 30, id=10)
- ('Carol', 22, id=2)
