一句话
慢 SQL 不要上来就加索引,而是先定位慢点,再用慢查询日志和 EXPLAIN 分析原因,最后从索引、SQL 写法、返回字段、扫描范围、分页和缓存等方向针对性优化,并用实际耗时验证。
1. 排查慢 SQL 的正确顺序
用户反馈"页面慢",不一定是 SQL 慢,可能是:
text
接口鉴权慢
远程调用慢
循环里多次查库
序列化数据太大
网络慢
SQL 本身慢
所以排查顺序应该是:
text
接口日志 / 链路追踪
↓
确认是否 SQL 慢
↓
慢查询日志定位具体 SQL
↓
EXPLAIN 分析执行计划
↓
针对原因优化
↓
验证优化效果
2. 第一步:先定位慢在哪里
不要一上来就说"加索引"。
先确认:
text
接口总耗时多少?
SQL 执行耗时多少?
是否有远程调用?
是否循环查库?
是否返回数据过大?
分工可以这样理解:
| 工具 | 作用 |
|---|---|
| 应用日志 / APM / 链路追踪 | 判断接口慢在哪里 |
| 慢查询日志 | 定位数据库里具体哪条 SQL 慢 |
| EXPLAIN | 分析这条 SQL 为什么慢 |
3. EXPLAIN 重点看哪些字段?
| 字段 | 重点含义 | 判断 |
|---|---|---|
type |
访问类型 | ALL 通常代表全表扫描,较差 |
key |
实际使用的索引 | NULL 表示没选索引 |
rows |
预估扫描行数 | 越大越可能慢,不是最终返回行数 |
filtered |
过滤比例 | 过滤比例低说明扫了很多无效数据 |
Extra |
额外信息 | 关注 Using filesort、Using temporary、Using index |
示例:
text
type = ALL
key = NULL
rows = 500000
Extra = Using where; Using filesort
说明:
text
全表扫描 + 没走索引 + 预估扫描 50 万行 + 额外排序
这条 SQL 慢的核心问题就是:
text
扫描行数太多 + 没走索引 + 排序没有利用索引
4. 示例 SQL:怎么设计联合索引?
SQL:
sql
SELECT *
FROM exam_record
WHERE user_id = ?
AND status = ?
AND create_time BETWEEN ? AND ?
ORDER BY create_time DESC
LIMIT 20;
条件类型:
text
user_id = ? 等值条件
status = ? 等值条件
create_time BETWEEN ? AND ? 范围条件
ORDER BY create_time DESC 排序字段
适合的联合索引:
sql
(user_id, status, create_time)
原因:
text
等值条件放前面,范围 / 排序字段放后面。
这样可以先通过 user_id 和 status 缩小范围,再按 create_time 范围扫描,并尽量利用索引顺序完成排序。
5. filesort 和回表不是一回事
filesort
关注的是:
text
排序是否能利用索引顺序?
如果索引是:
sql
(user_id, status, create_time)
查询条件:
sql
WHERE user_id = ? AND status = ?
ORDER BY create_time DESC
通常可以利用索引顺序,避免额外 filesort。
回表
关注的是:
text
查询结果字段是否被索引覆盖?
如果是:
sql
SELECT *
即使 WHERE 和 ORDER BY 走了联合索引,也通常需要回表取整行数据。
如果只查:
sql
SELECT id, user_id, status, create_time
并且索引是:
sql
(user_id, status, create_time)
在 InnoDB 里二级索引叶子节点带主键 id,可能形成覆盖索引,减少回表。
6. 有索引为什么还慢?
记住:
text
有索引 ≠ 一定快
走索引 ≠ 扫描行数一定少
可能原因:
- 命中范围太大,
rows仍然很高 - 字段选择性差,比如
status=已完成占 90% - 时间范围太大,比如一次查一年
SELECT *导致大量回表- 深分页导致扫描和丢弃大量数据
- 统计信息不准,优化器选错索引
- SQL 写法导致索引失效
7. 什么是选择性?
选择性可以理解为:
text
一个条件能筛掉多少数据。
| 条件 | 选择性 |
|---|---|
user_id = 1001 |
通常较高 |
status = 已完成 |
通常较低 |
create_time = 今天 |
较高 |
create_time BETWEEN 一年前 AND 今天 |
较低 |
索引设计时,通常希望高选择性的字段靠前,但也要结合等值、范围、排序和实际查询模式综合判断。
8. 最左前缀与不同查询场景
索引:
sql
(user_id, status, create_time)
适合查询 A:
sql
WHERE user_id = ?
AND status = ?
ORDER BY create_time DESC
LIMIT 20;
不太适合查询 B:
sql
WHERE status = ?
ORDER BY create_time DESC
LIMIT 20;
原因:查询 B 跳过了最左列 user_id,无法充分利用联合索引。
对于查询 B,可能需要单独设计:
sql
(status, create_time)
结论:
不是一个大联合索引解决所有问题,而是按高频查询场景设计索引。
9. 常见优化方向
| 问题 | 优化方向 |
|---|---|
| 没走索引 | 设计合适索引,调整 SQL 条件 |
| 联合索引顺序不合理 | 等值在前,范围 / 排序在后 |
| 回表太多 | 减少 SELECT *,尝试覆盖索引 |
| 排序慢 | 让 ORDER BY 字段匹配索引顺序 |
| 扫描范围大 | 缩小时间范围,提高筛选条件选择性 |
| 深分页 | 延迟关联、基于游标分页、记录上次最大/最小 ID |
| 索引失效 | 避免函数、隐式类型转换、前导 %、违反最左前缀 |
| 热点查询 | 加缓存,但要考虑一致性和过期策略 |
10. 索引失效检查清单
常见索引失效场景:
text
1. 不满足最左前缀原则
2. 对索引列使用函数,如 DATE(create_time)
3. 隐式类型转换,如 VARCHAR 字段传数字
4. LIKE 以 % 开头
5. OR 条件中有非索引字段
6. 排序字段和索引顺序不匹配
🎙 面试回答模板
text
如果项目里遇到一条慢 SQL,我一般会先定位慢点,不会一上来就直接加索引。首先通过接口日志、链路追踪或者 SQL 耗时日志,判断是接口整体链路慢,还是数据库查询慢。如果确认是 SQL 慢,再通过慢查询日志定位具体 SQL。拿到 SQL 后,我会用 EXPLAIN 看执行计划,重点关注 type、key、rows、filtered 和 Extra,判断有没有走索引、是否全表扫描、预估扫描行数是否过大、是否出现 Using filesort 或 Using temporary。
优化时会结合查询条件设计合适的联合索引,比如等值条件放前面,范围和排序字段放后面;同时避免 SELECT *,尽量使用覆盖索引减少回表。如果已经走了索引但仍然慢,我会继续看是不是时间范围太大、字段选择性太低、分页过深,或者数据量本身太大。最后还要检查 SQL 写法是否导致索引失效,比如函数计算、隐式类型转换、前导模糊 LIKE、不符合最左前缀原则等。优化后再通过 EXPLAIN 和实际接口耗时对比验证效果。
参考来源
- MySQL 官方文档:EXPLAIN Output Format
- MySQL 官方文档:Index Optimization
- 21-B+ 树与 MySQL 索引结构
- 22-聚簇索引回表与覆盖索引
- 23-最左前缀原则与联合索引设计
- 24-索引失效 6 种场景
- 02_Wiki【结构化知识库】/工作实践/简历与面试备战/14 天面试复习打卡/01-Java 基础复习/Day08-MySQL 事务与优化.md