1. 怎么查看一条语句是否走了索引?#
分析
考察 EXPLAIN 执行计划输出的信息。

执行计划常见参数有:
possible_keys字段表示可能用到的索引;key字段表示实际用的索引,如果这一项为NULL,说明没有使用索引;key_len表示索引的长度;rows表示扫描的数据行数;type表示数据扫描类型,需要重点关注。
type 字段描述了找到所需数据时使用的扫描方式,常见扫描类型的执行效率从低到高的顺序为:
ALL(全表扫描):最差的情况,采用全表扫描的方式。index(全索引扫描):index和ALL差不多,只不过index对索引表进行全扫描,好处是不再需要对数据进行排序,但是开销依然很大。所以如果EXPLAIN中看到结果为index,并不是代表用到了索引快速查找。range(索引范围扫描):一般在WHERE子句中使用<、>、IN、BETWEEN等关键词,只检索给定范围的行,属于范围查找。从这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到range这一类别以及以上的type访问方式。ref(非唯一索引扫描):ref类型表示采用了非唯一索引,或者唯一索引的非唯一性前缀,返回数据可能是多条。因为虽然使用了索引,但该索引列的值并不唯一,有重复。这种查询能用索引快速查找到第一条数据,但仍然不能停止,要进行目标值附近的小范围扫描。eq_ref(唯一索引扫描):通常使用在多表联查中。例如两张表联查,关联条件是两张表的user_id相等,且user_id是唯一索引,那么使用EXPLAIN查看执行计划时,type就会显示eq_ref。const(结果只有一条的主键或唯一索引扫描):表示使用了主键或者唯一索引与常量值进行比较。比如SELECT name FROM product WHERE id = 1。const和eq_ref都使用了主键或唯一索引,不过const是与常量比较,查询效率会更快,而eq_ref通常用于多表联查中。
Extra 显示的结果中,有几个重要的参考指标:
Using filesort:当查询语句中包含ORDER BY操作,而且无法利用索引完成排序操作时,就不得不选择相应的排序算法进行排序,甚至可能会通过文件排序,效率很低,要避免这种问题出现。Using temporary:使用了临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序ORDER BY和分组查询GROUP BY,效率低,要避免这种问题出现。Using index:所需数据只需在索引中即可全部获得,不需要再到表中取数据,也就是使用了覆盖索引,避免了回表操作,效率不错。
回答
可以通过 EXPLAIN 查看 SQL 的执行计划,关注 type 字段。这个字段表明 SQL 扫描的方式,如果 type 字段不是 ALL 或者 index,就代表是索引扫描的方式,这种情况就代表 SQL 走了索引。
并且我们还可以通过 key 字段,看这条查询用了哪个索引字段来走索引,如果 key 为 NULL,也代表没有走索引。
2. Extra 字段中的 Using index 和 Using where 的区别?#
分析
考察 EXPLAIN 执行计划输出的信息。
Using index:意味着 MySQL 能够使用覆盖索引(Covering Index)来避免访问表的行。覆盖索引是指一个查询的所有列都包含在索引中,因此查询可以仅通过查看索引来获取所需的信息,无需再去访问表的行,这通常能提高查询性能。Using where:意味着 MySQL 服务器将在存储引擎检索行后再进行过滤。换句话说,存储引擎返回的行并不一定满足WHERE子句的条件,MySQL 服务器需要对这些行进行额外检查。
这两者并不互斥,可以同时出现在 Extra 字段中,这取决于查询的具体情况。
回答
Using index 表示查询使用了索引覆盖,不会回表,这可以提高查询效率。
Using where 表示 MySQL 的存储引擎返回给 Server 层的数据并不一定满足 WHERE 子句的条件,所以 MySQL 从存储引擎拿到的数据,还得在 Server 层进行 WHERE 子句的条件判断,来过滤出最终 SQL 所需要查询的数据。
3. 怎么找到慢 SQL?#
分析
考察慢查询日志的应用。
回答
可以开启慢查询日志,MySQL 就会自动将执行比较慢的 SQL 语句记录在慢查询日志中。具体多慢可以自己设置,比如设置 3 秒,那么 MySQL 就会将执行超过 3 秒的 SQL 语句记录在慢查询日志中。
4. 如何优化慢 SQL?#
分析
常见 SQL 优化的方法:
- 优化数据访问:
LIMIT子句缩减数据行数,避免SELECT *。 - 拆分查询:分而治之,将一个大查询拆分多个小查询,每个小查询只返回一部分查询结果。
- 覆盖索引:当索引中的列包含所有查询中需要使用的列时,可以避免回表。
- 避免索引失效:检查 SQL 是否因为写得不合理导致索引失效。
- 分解联表查询:让业务层分多个查询来聚合,或者增加冗余字段减少联表查询。
- 排序优化:对于有排序场景,如果
Extra显示filesort,就需要考虑对排序字段建立索引,避免文件排序。
回答
- 优化数据访问:先确认这条查询语句是否查询了不必要的数据行,可以通过
LIMIT子句缩减查询返回的数据行数。如果查询语句用了SELECT *,需要改进 SQL 语句,只返回需要查询的列。 - 切分查询:针对一个大查询可以拆分多个小查询,每个小查询只返回一部分查询数据。比如删除一千万行数据,可以改进成分批删除,每一次只删除一批数据,然后睡眠一下,再删除下一批,这样可以将一次性的压力分散到一个很长的时间段中,不仅可以降低对服务器的性能影响,还可以大大减少删除时锁的持续时间。
- 覆盖索引:如果没有索引字段,就需要考虑建立索引,或者建立联合索引,通过覆盖索引的查询,避免回表查询,提高查询性能。
- 避免索引失效:检查 SQL 语句有没有问题,比如对索引进行了计算和函数操作、联合索引没有遵循最左匹配原则等,这些场景都会导致索引失效,这时候需要修改 SQL,避免索引失效。
- 分解联表查询:针对联表查询的 SQL 语句,可以将联表查询分解成多个单表查询语句,然后在业务层来聚合数据,或者增加冗余字段减少联表查询。
- 排序优化:针对
ORDER BY排序操作,如果执行计划的Extra显示了文件排序,可以对排序字段和其他字段建立联合索引。因为索引数据是天然有序的,对排序字段进行排序操作时,就不需要文件排序了,提高查询性能。
5. 深分页场景如何优化?#
分析
在系统需要实现分页操作时,通常都是用 LIMIT 加上偏移量实现的。如果偏移量太大,就存在性能问题。比如下面这样的查询,MySQL 最左叶子节点开始向右扫描 10020 条记录,时间复杂度为 \(O(n)\),然后只返回 20 条给客户端,前面 10000 条记录都将被抛弃。
SELECT * FROM t_player ORDER BY score DESC LIMIT 10000, 20;
如果使用了二级索引,这种场景性能损失会加剧,因为对于前 10000 个不需要的数据,MySQL 每次也要回表去查找,这就导致了 10000 次随机 I/O,会很费劲。
优化方式:
- 减少扫描次数:从业务上改进,将“第几页”改成“下一页”,先记录上一页最后一条记录的
id或排序字段值,然后下次就直接从该记录的位置开始扫描,这样就避免 MySQL 扫描大量不需要的行然后再抛弃掉的问题。
-- 记录上一页的最后一条记录的 score 为 prev_score
SELECT score FROM t_player ORDER BY score DESC LIMIT 20;
-- 下一页
SELECT score FROM t_player WHERE score < prev_score ORDER BY score DESC LIMIT 20;
- 减少回表:如果要遵循第几页的方案,可以通过 SQL 的拆分来达到目的。思路是先从条件查询中,查找数据对应的数据库唯一
id值,因为主键在辅助索引上就有,所以不用回扫聚簇索引的磁盘上拉取。如此一来,OFFSET部分均不回表查聚簇索引,只有LIMIT出来的 20 个主键id会去查询聚簇索引,这样只会有 20 次 I/O。
SELECT *
FROM t_player
WHERE id IN (
SELECT id FROM t_player ORDER BY score LIMIT 10000, 20
);
之前有同学做过实验,5000w 数据的场景下,针对二级索引分页的场景,如果使用 LIMIT n, m 分页方式,查询速度是 286 秒。如果采用这种二次优化,通过子查询来减少回表次数,查询速度只需要 0.7 秒。
回答
分页最简单的实现是使用 LIMIT 字节句,比如每页显示 10 条内容,第一页就是 LIMIT 10,第二页就是 LIMIT 10, 10,第三页就是 LIMIT 20, 10。但是这种方式,在深分页的场景,存在严重的性能问题,比如 LIMIT 10000, 20 这样的查询,这时候 MySQL 最左叶子节点开始向右扫描 10020 条记录,时间复杂度为 \(O(n)\),然后只返回 20 条给客户端,前面 10000 条记录都将被抛弃。如果是使用了二级索引,这种场景性能损失会加剧,因为对于前 10000 个不需要的数据,MySQL 每次也要回表去查找,这就导致了 10000 次随机 I/O。
我能想到这两种优化方式:
- 可以在业务上改进,将“第几页”改成“下一页”,先记录上一页的最后一条记录的
id,然后下次就直接从该记录的位置开始扫描,这样就避免 MySQL 扫描大量不需要的行然后再抛弃掉的问题。 - 如果要遵循第几页的方案,可以通过覆盖索引加子查询方式改进。子查询语句主要查询分页数据对应的数据库唯一
id值,因为主键在辅助索引上就有,所以子查询可以不用回表。然后主查询再根据子查询返回的id,进行索引查询完整的数据行。
6. 如果 SQL 和索引都没问题,查询还是很慢怎么办?#
分析
需要发散下思维,往架构优化方向思考。
回答
- 分批查询:针对一个大查询可以拆分多个小查询,每个小查询只返回一部分查询数据。
- 增加缓存:针对频繁读取的热点数据,我们可以放到 Redis 缓存,避免每次都要请求 MySQL。
- 分表:如果表的数据量很大,比如表数据千万级别了,这时候可以考虑分表,通过减少每次查询数据总量来解决数据查询缓慢的问题。
- 主从复制:针对读多写少的场景,我们可以搭建 MySQL 主从模式来分摊读请求的流量。
- 分库:针对写多读少的场景,单库的性能无法抗住高并发流量,就需要进行分库,把并发请求分散到多个实例中去。