前言
在 MySQL 的性能优化体系里,索引是最重要、也最容易被误用的一环。很多慢查询并不是因为 SQL 写得有多复杂,而是因为没有合理使用索引;也有很多线上事故,表面看是"数据库慢了",本质上却是索引设计不合理、查询条件失效、索引过多导致写入性能下降。
如果把数据库比作图书馆,那么表数据就是一本本书,索引就是目录。没有目录,你要找一本书只能一本本翻;有了目录,你可以迅速定位到目标内容。但目录也不是越多越好,目录太多不仅占空间,还会让每次新增、修改、删除都变慢。
一、什么是索引
索引(Index)是一种帮助数据库快速定位数据 的数据结构。
在没有索引的情况下,数据库通常需要进行全表扫描 :从第一行找到最后一行,逐行判断是否满足条件。
而有了索引之后,数据库可以先在索引结构中快速找到目标记录的位置,再去表中取出数据,从而大幅减少扫描量。
1.1 索引的本质
索引的本质是:
用额外的存储空间,换取更快的查询速度。
这是一种典型的"空间换时间"策略。
但索引并不是免费午餐。它有三类成本:
- 存储成本:索引本身要占磁盘空间
- 维护成本:数据插入、更新、删除时,索引也要同步维护
- 设计成本:索引设计不合理,反而会拖慢系统
所以,索引不是"加得越多越好",而是要按查询模式设计。
二、MySQL 中常见的索引类型
在 MySQL 中,索引不止一种。从不同角度看,可以分为以下几类。
2.1 按逻辑用途分类
1. 主键索引(Primary Key)
主键索引是最重要的索引,具有唯一性,不能为 NULL。
InnoDB 中,主键索引通常就是聚簇索引。
2. 唯一索引(Unique Index)
保证列或列组合的值唯一,但允许出现多个 NULL(具体行为依赖引擎与版本)。
常用于用户名、邮箱等字段。
3. 普通索引(Index)
最常见的索引类型,只是提高查询效率,不保证唯一性。
4. 联合索引(Composite Index)
由多个列共同组成的索引。
比如 (user_id, status, created_at)。
三、MySQL 索引底层结构
MySQL 的索引实现方式会因存储引擎而不同。
最常见的是 InnoDB,它默认使用 B+ 树 作为索引结构。
3.1 为什么是 B+ 树
B+ 树适合数据库索引,原因主要有:
- 树的高度低,查找次数少
- 节点可以存放更多键值,减少磁盘 I/O
- 叶子节点有序并相互连接,适合范围查询
- 查询稳定性高,性能可预测
相比之下:
- 二叉树层级太深
- 哈希索引不适合范围查询
- 红黑树虽然平衡,但不如 B+ 树适合磁盘场景
3.2 B+ 树索引示意图
下面这张图可以直接放进你的文章里:
Root 根节点
Internal Node 1
Internal Node 2
Leaf 1: 1,3,5
Leaf 2: 7,9,11
Leaf 3: 13,15,17
Leaf 4: 19,21,23
3.3 B+ 树的特点
- 非叶子节点只存键值和指针,不存完整数据
- 所有数据都保存在叶子节点
- 叶子节点之间有链表指针,便于范围扫描
- 查找任意记录,最终都要落到叶子层
这意味着:
主键查询、范围查询、排序查询,都能从 B+ 树中受益。
四、InnoDB 的聚簇索引与二级索引
理解 InnoDB 索引,必须掌握一个核心概念:聚簇索引(Clustered Index)。
4.1 什么是聚簇索引
在 InnoDB 中,数据行本身就存储在主键索引的叶子节点上 。
也就是说,主键索引的叶子节点不仅保存主键值,还保存整行数据。
这就是为什么 InnoDB 的主键索引被称为聚簇索引:
索引和数据是"聚在一起"的。
4.2 什么是二级索引
除了主键索引,其他索引都称为二级索引 。
二级索引的叶子节点不保存整行数据,只保存:
- 二级索引列的值
- 对应主键值
因此,通过二级索引查数据时,通常需要经历两步:
- 先在二级索引中找到主键值
- 再根据主键回到聚簇索引中取整行数据
这个过程叫 回表。
4.3 聚簇索引与二级索引示意图
二级索引:name
Alice -> ID=1
Bob -> ID=2
Carol -> ID=3
聚簇索引 / 主键索引
ID=1 -> row1
ID=2 -> row2
ID=3 -> row3
4.4 为什么主键建议短而稳定
因为主键越长:
- 聚簇索引的每个叶子页能容纳的记录越少
- 二级索引里存储的主键引用也会变大
- B+ 树层级可能更高,I/O 更多
所以主键设计建议:
- 尽量短
- 尽量稳定
- 尽量有序
- 避免频繁变动
常见推荐:
- 自增整数主键
- 雪花 ID
- 分布式场景下有序 ID
而以下主键通常不理想:
- 很长的字符串主键
- 频繁更新的业务字段
- 无序随机 UUID 作为主键
五、联合索引与最左前缀原则
联合索引是生产环境中最常用、也最容易误解的索引形式之一。
5.1 什么是联合索引
比如建立索引:
sql
CREATE INDEX idx_user_status_time ON orders(user_id, status, created_at);
这个索引按顺序组织数据,数据库会优先按 user_id 排序,再按 status,再按 created_at。
5.2 最左前缀原则
联合索引是否生效,遵循最左前缀原则 :
查询条件必须从索引的最左列开始,才能充分利用索引。
对于 (user_id, status, created_at):
可用索引的查询
sql
SELECT * FROM orders WHERE user_id = 1001;
SELECT * FROM orders WHERE user_id = 1001 AND status = 1;
SELECT * FROM orders WHERE user_id = 1001 AND status = 1 AND created_at > '2026-01-01';
部分可用或无法充分利用的查询
sql
SELECT * FROM orders WHERE status = 1;
SELECT * FROM orders WHERE created_at > '2026-01-01';
5.3 为什么要遵守最左前缀
联合索引在 B+ 树中的排序是按列顺序进行的。
如果查询跳过了最左列,数据库就无法直接定位到有序范围,只能扫描更多数据。
六、哪些情况会导致索引失效
索引失效并不等于索引不存在,而是查询条件无法有效使用索引。
下面是最常见的失效场景。
6.1 对索引列做函数或表达式运算
sql
SELECT * FROM users WHERE DATE(create_time) = '2026-04-17';
这里对 create_time 做了函数运算,数据库通常无法直接使用索引。
更好的写法是:
sql
SELECT * FROM users
WHERE create_time >= '2026-04-17 00:00:00'
AND create_time < '2026-04-18 00:00:00';
6.2 隐式类型转换
sql
SELECT * FROM users WHERE phone = 13800138000;
如果 phone 是字符串类型,而你传入的是数字,可能触发隐式转换,导致索引失效。
6.3 LIKE 前置通配符
sql
SELECT * FROM users WHERE name LIKE '%tom';
前面有 %,数据库无法从索引头部开始定位,只能扫描。
如果是:
sql
SELECT * FROM users WHERE name LIKE 'tom%';
则通常仍可使用索引。
6.4 OR 条件使用不当
sql
SELECT * FROM users WHERE id = 1 OR nickname = 'tom';
如果两个条件都没有合适索引,或者其中一个索引选择性差,可能导致全表扫描。
6.5 范围查询后面的列难以继续利用
在联合索引中,如果前面列使用了范围查询:
sql
SELECT * FROM orders
WHERE user_id = 1001 AND created_at > '2026-01-01' AND status = 1;
如果 created_at 是范围条件,那么后面的 status 列往往难以继续充分利用。
6.6 选择性太差
如果某列只有极少数取值,比如 gender 只有 0/1 两种值,这种列单独建索引通常意义不大。
因为即使走索引,也要扫描大量数据,优化器可能直接选择全表扫描。
七、索引优化的核心原则
索引优化的目标不是"让所有查询都走索引",而是:
让最频繁、最耗时、最有价值的查询,以最小代价拿到最少的数据。
7.1 先优化查询,再优化索引
很多人一上来就建索引,结果越建越乱。
正确顺序应该是:
- 找出慢查询
- 分析访问模式
- 看执行计划
- 再设计索引
- 验证效果
7.2 索引要服务于高频查询
优先优化:
- 频繁执行的 SQL
- 返回数据量大的查询
- 参与核心业务链路的查询
- 影响用户体验的慢查询
不要为了一个偶尔执行的报表 SQL 去给整个表乱建一堆索引。
7.3 高选择性的列更适合建索引
选择性越高,索引效果越好。
比如:
- 用户 ID
- 订单号
- 手机号
- 邮箱
- 唯一业务编码
这些列通常比"状态字段""布尔字段"更适合单独建索引。
7.4 尽量让索引覆盖查询
如果查询所需字段都能从索引中直接拿到,不必回表,这种索引叫 覆盖索引。
例如:
sql
SELECT user_id, status
FROM orders
WHERE user_id = 1001;
如果有索引 (user_id, status),那么数据库只需要扫描索引就能返回结果,无需回表。
八、什么是覆盖索引
8.1 覆盖索引的优势
覆盖索引的好处主要有:
- 减少回表次数
- 降低随机 I/O
- 查询更快
- 缓解主键索引压力
8.2 覆盖索引示意图
查询 user_id, status
联合索引 (user_id, status)
直接返回结果
查询 user_id, status, amount
联合索引 (user_id, status)
需要回表获取 amount
8.3 如何设计覆盖索引
比如业务常查:
sql
SELECT id, status, created_at
FROM orders
WHERE user_id = 1001 AND status = 1;
可以考虑设计:
sql
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);
这样不仅能支持查询条件,还能尽量减少回表。
但要注意,覆盖索引不是让你无限扩大索引字段。
索引太宽会导致:
- 索引占空间更大
- 写入更慢
- 缓存命中率变差
所以覆盖索引是"有选择地设计",不是"全字段塞进去"。
九、索引顺序怎么设计
联合索引的顺序非常重要。
一般来说,字段顺序应当根据以下因素决定:
- 等值查询列优先
- 高区分度列优先
- 常用于排序/分组的列
- 范围查询列放后面
9.1 经验原则
假设有如下查询:
sql
SELECT * FROM orders
WHERE user_id = ?
AND status = ?
AND created_at BETWEEN ? AND ?
ORDER BY created_at DESC
可能的索引顺序是:
sql
(user_id, status, created_at)
原因是:
user_id和status是等值过滤created_at是范围和排序字段- 顺序合理时能减少扫描并帮助排序
9.2 不合理的例子
如果你建成:
sql
(created_at, status, user_id)
那么等值过滤就无法充分发挥作用,查询性能通常会差很多。
十、慢查询如何分析
做索引优化,不能只靠经验,必须结合执行计划和慢查询日志。
10.1 慢查询日志
MySQL 提供慢查询日志,可以记录执行时间超过阈值的 SQL。
通过慢查询日志,可以找到最值得优化的语句。
10.2 EXPLAIN 的作用
EXPLAIN 可以查看 SQL 的执行计划,判断是否走索引、扫描了多少行、是否使用了临时表和文件排序等。
示例:
sql
EXPLAIN SELECT * FROM orders WHERE user_id = 1001;
重点关注字段:
typekeyrowsExtra
10.3 执行计划常见含义
type
表示访问类型,性能通常从好到坏大致如下:
systemconsteq_refrefrangeindexALL
其中 ALL 通常意味着全表扫描。
key
实际使用的索引名。
如果为 NULL,说明没用上索引。
rows
估算需要扫描的行数。
越少越好。
Extra
常见的有:
Using index:使用了覆盖索引Using where:还需要额外过滤Using temporary:使用临时表Using filesort:出现文件排序
如果 Using temporary 和 Using filesort 同时出现,通常说明需要重点优化。
十一、索引优化实战案例
下面通过几个典型场景来说明索引优化思路。
11.1 场景一:根据用户 ID 查询订单列表
SQL:
sql
SELECT id, order_no, status, created_at
FROM orders
WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 20;
问题分析
user_id是过滤条件created_at需要排序- 结果只需要少量字段
推荐索引
sql
CREATE INDEX idx_user_created ON orders(user_id, created_at);
如果还希望减少回表,可以进一步考虑把 status 也纳入索引:
sql
CREATE INDEX idx_user_created_status ON orders(user_id, created_at, status);
但是否加入,要看实际查询字段和写入成本。
11.2 场景二:按状态和时间统计订单
SQL:
sql
SELECT COUNT(*)
FROM orders
WHERE status = 1
AND created_at >= '2026-04-01'
AND created_at < '2026-05-01';
推荐索引
sql
CREATE INDEX idx_status_created ON orders(status, created_at);
原因
status是等值created_at是范围- 两者组合适合统计类查询
11.3 场景三:模糊搜索用户名
SQL:
sql
SELECT * FROM users WHERE username LIKE 'tom%';
情况
这种前缀匹配可使用索引:
sql
CREATE INDEX idx_username ON users(username);
但如果写成:
sql
SELECT * FROM users WHERE username LIKE '%tom%';
普通 B+ 树索引就很难发挥作用。
这种需求更适合:
- 全文索引
- 搜索引擎
- 专门的检索系统
十二、索引越多越好吗
答案是否定的。
索引过多常常会带来副作用。
12.1 索引过多的坏处
1. 插入变慢
每次插入数据时,相关索引都要更新。
2. 更新变慢
如果更新的列包含索引字段,索引也要调整。
3. 删除变慢
删除行时,同样要维护索引结构。
4. 占用更多空间
索引本身会占磁盘和缓存。
5. 优化器选择变复杂
索引太多时,优化器的选择空间更大,计划评估也更复杂。
12.2 索引设计的平衡
索引优化的本质是平衡:
- 查询性能
- 写入性能
- 存储成本
- 维护复杂度
所以,真正好的索引设计不是"最多",而是"刚刚好"。
十三、索引优化的实用建议
下面是一些非常实用的索引优化建议。
13.1 给高频查询建索引
优先服务最常见的查询,而不是所有可能的查询。
13.2 复合条件尽量设计联合索引
不要把多个单列索引堆在一起,很多时候联合索引更有效。
13.3 控制索引长度
尤其是字符串索引,尽量避免无意义的超长索引。
13.4 避免在索引列上做函数运算
把函数放到常量一侧,或者改写查询条件。
13.5 注意排序字段
如果业务经常按某个字段排序,可以把它纳入联合索引。
13.6 定期清理无用索引
长期不用的索引不仅浪费空间,还会拖慢写入。
13.7 使用 EXPLAIN 验证
不要凭感觉判断,实际执行计划最重要。
十四、常见误区
14.1 误区一:只要建了索引就一定快
错。
索引是否生效、是否合适、是否回表、是否选择性高,都很关键。
14.2 误区二:单列索引叠加就等于联合索引
错。
多个单列索引通常不能替代一个设计合理的联合索引。
14.3 误区三:索引列越多越好
错。
索引宽度过大,会影响写入和缓存。
14.4 误区四:所有查询都要走索引
错。
当数据量很小,或者查询返回大部分数据时,全表扫描反而可能更快。
14.5 误区五:主键越随机越好
错。
随机主键会导致页分裂和写入抖动,尤其在 InnoDB 中更明显。
十五、如何判断一个索引是否值得保留
一个索引是否有价值,通常看以下几个问题:
- 这个索引是否被高频使用?
- 它是否真正降低了查询成本?
- 它是否导致了明显的写入开销?
- 它是否与其他索引重复?
- 它是否适合当前业务查询模式?
如果一个索引:
- 很少被用到
- 与其他索引重叠
- 占用较大空间
- 维护成本高
那就应该考虑删除或合并。
十六、MySQL 索引优化的完整思路
一个完整的索引优化流程可以概括为:
第一步:定位慢 SQL
通过慢查询日志、APM、监控系统找到慢 SQL。
第二步:分析业务场景
明确这条 SQL 是查详情、分页、统计、筛选还是排序。
第三步:查看执行计划
使用 EXPLAIN 看是否走索引、扫描多少行、有没有回表。
第四步:设计索引
根据过滤条件、排序条件、覆盖字段、选择性来设计索引。
第五步:验证效果
对比优化前后的执行时间、扫描行数和执行计划。
第六步:观察写入影响
索引优化不能只看查询,要观察插入、更新、删除是否变慢。