索引是 MySQL 优化查询最核心的手段。很多开发只会简单加索引,不懂底层 B + 树结构,经常写出索引失效的慢 SQL。
本文从索引的作用、B + 树结构讲起,详解 InnoDB 聚簇索引、非聚簇索引,重点讲解覆盖索引原理,附带索引使用规则、踩坑清单,适合后端开发学习。
前言
数据库查询如果不加索引,执行 SQL 时会全表扫描,逐条对比数据,数据量大的时候性能极差。 索引本质就是帮助数据库快速查找数据的一种数据结构。 MySQL 有多种存储引擎,我们日常开发几乎都用InnoDB 引擎,InnoDB 索引底层全部使用B + 树。
注意:MyISAM 也是 B + 树,但 InnoDB 和 MyISAM 索引存储结构差异很大,本文默认讲 InnoDB。
一、为什么选用 B + 树,不用二叉树、红黑树、B 树?
磁盘 IO 是数据库查询最大的性能瓶颈。索引存储在磁盘,读取节点就会产生一次磁盘 IO。
- 二叉搜索树:数据倾斜会退化成链表,树高不可控;
- 红黑树:平衡二叉树,树高依然高,数据量大 IO 次数多;
- B 树:多路平衡树,数据和索引放在同一个节点;
- B + 树:多路平衡树,所有数据都存放在叶子节点,非叶子节点只存索引键 + 指针。
B + 树核心特点:
- 非叶子节点只存储索引值和指针,不存储完整行数据,单个节点可以存放更多索引,树的高度很低;千万级数据,B + 树树高一般只有 2~3 层;
- 所有叶子节点在同一层,叶子节点通过双向链表相连,范围查询非常方便;
- 数据全部在叶子节点,查询任何数据都要走到叶子节点,IO 次数稳定。
二、聚簇索引(主键索引)
InnoDB 中,聚簇索引就是主键索引,把整行数据保存在索引的叶子节点。
聚簇:数据和索引放在一起,索引的叶子节点就是真实的表数据。
规则
- 表必须有聚簇索引:
- 如果定义主键,主键就是聚簇索引;
- 没有主键,自动选唯一非空索引作为聚簇索引;
- 没有唯一非空索引,MySQL 自动生成隐藏的 rowid 作为聚簇索引。
- 一张表只能有一个聚簇索引。
查找流程
执行select * from user where id = 10;,id 是主键(聚簇索引)
- 从 B + 树根节点向下查找,对比索引值;
- 一直找到叶子节点;
- 叶子节点直接存放完整的一行数据,直接返回。
优点:
- 主键查询速度最快,找到索引就拿到全部数据;
- 主键范围查询,叶子节点链表可以顺序遍历。
缺点:
- 主键尽量不要用无序自增 ID。如果主键随机(如 UUID),插入数据会频繁分裂叶子节点,造成页分裂,性能下降;
- 聚簇索引占用空间大。
三、非聚簇索引(二级索引 / 普通索引)
普通索引、唯一索引都属于非聚簇索引。
非聚簇索引叶子节点不存完整行数据,只存储索引列的值 + 主键值。
查找流程(回表)
给 name 字段建立普通索引:create index idx_name on user(name);执行 SQL:select * from user where name='张三';
- 在 name 的 B + 树(二级索引)查找,找到叶子节点;
- 叶子节点拿到主键 id;
- 拿着主键 id,再去聚簇索引 B + 树查询一次完整行数据,这个过程叫做回表。
重点:二级索引查询,默认需要两次 B + 树查找:二级索引树 → 聚簇索引树(回表)。
四、覆盖索引(避免回表,优化神器)
覆盖索引:查询需要的所有字段,全部包含在二级索引里面,不需要回表查询聚簇索引。查询的列,全部在二级索引叶子节点能拿到,就跳过回表操作,性能大幅提升。
示例
表:user (id,name,age,address) 索引:idx_name_age (name,age)联合索引
SQL:
select name,age from user where name='张三';查询只需要 name、age,这两个字段都在联合索引idx_name_age的叶子节点,不需要回表拿主键再查聚簇索引,这就是覆盖索引。
不满足覆盖索引:
select name,age,address from user where name='张三';address 不在二级索引,拿到主键后必须回表查询。
如何判断是否触发覆盖索引:explain 执行计划里,Extra 字段显示
Using index,代表命中覆盖索引。
覆盖索引优点:
- 避免回表,减少一次 B + 树查询,IO 减半;
- 二级索引叶子节点数据量小,内存可以缓存更多数据。
注意:不要为了覆盖索引盲目创建大量联合索引,索引会占用磁盘,并且降低插入、更新、删除性能。
五、聚簇索引 vs 非聚簇索引对比表
| 对比项 | 聚簇索引(主键索引) | 非聚簇索引(二级索引) |
|---|---|---|
| 叶子节点存储内容 | 整行全部数据 | 索引列 + 主键 ID |
| 数量 | 一张表只能 1 个 | 可以多个 |
| 查询 | 找到索引直接拿到全部数据 | 默认需要回表 |
| 主键过大影响 | 主键越大,二级索引占用空间越大 | 叶子要存储主键,主键越长索引体积越大 |
六、索引最左前缀原则(补充)
建立联合索引idx_a_b_c(a,b,c)索引生效顺序:从左向右匹配,遇到范围查询 (> < between like 前缀模糊) 就停止匹配。
- where a=1 → 命中
- where a=1 and b=2 → 命中
- where b=2 and a=1 → 命中(MySQL 优化器会调整顺序)
- where a>1 and b=2 → a 是范围,b 索引失效
七、索引失效常见场景
- 索引列上做函数运算、隐式类型转换;
- like '% 关键字 ',左模糊查询,索引失效;
- 联合索引不满足最左前缀;
- 使用 or,or 两边有字段没加索引;
- MySQL 优化器判断全表扫描比索引更快,会放弃索引。
八、开发踩坑汇总
- 主键推荐使用自增整型,不要 UUID,容易造成页分裂;
- 主键字段不要设计太长,所有二级索引都会保存主键,主键越长二级索引体积越大;
- 不要创建过多索引,索引会减慢增删改;
- 覆盖索引是优化手段,不要为了覆盖索引随意创建超长联合索引;
- 不要在低基数列(性别、状态只有少量枚举值)建立索引,索引几乎无效。
九、问答
Q1:InnoDB 聚簇索引和非聚簇索引区别?
A:聚簇索引叶子节点存储完整行数据,一张表只能一个;非聚簇索引叶子存储索引列 + 主键,查询到主键后需要回表到聚簇索引获取完整数据。
Q2:什么是回表?什么是覆盖索引?
A:回表:二级索引查询只能拿到主键,再去主键索引查找完整数据。覆盖索引:查询所需字段全部存在二级索引,不需要回表,explain 显示 Using index。
Q3:B + 树对比 B 树优势?
A:B + 树非叶子节点不存数据,节点能存放更多索引,树更低 IO 更少;叶子节点链表,范围查询方便。
Q4:一张表可以有多个聚簇索引吗?
A:不能,InnoDB 一张表只能有一个聚簇索引。
结语
InnoDB 索引底层基于 B + 树,聚簇索引把数据和主键索引绑定在一起,二级索引默认会回表。覆盖索引是重要优化手段,通过建立合适联合索引避免回表,提升查询性能。 写 SQL 的时候,结合 explain 看执行计划,判断索引是否命中、是否触发覆盖索引,避免写出慢 SQL。