1.前情提要
小凯是一位程序员,今天写好了接口,和前端对接好,然后自测了几种特殊情况,都没问题,把功能发布上线了,然后开始思考今天晚上下班吃什么好呢。
凯哥!这个接口为什么会返回这么慢,前端姐姐说。小凯看了接口的返回信息是接口返回超时,是因为某个sql的执行时间太长导致后端已经没有任何数据返回了。
2.排查问题,遇到瓶颈
-
看看mysql是否还正常运行
小凯打开了线上的项目,然后模拟了一下登录,发现登录正常,小凯松了口气,还好数据库没崩
-
查看服务器的内存和cpu的使用情况
小凯登录服务器,用top命令查看了Java进程和mysql进程的cpu和内存的占比情况,以及整个服务器的磁盘资源是否还有剩余,发现各项占比都正常,服务器的资源都很充足。
-
看看是否是sql本身的问题
确保服务没问题了以后,有问题的就是sql本身了。小凯把线上超时的sql复制,然后放到本地的测试环境中运行,发现响应时间是正常的,耗时100ms左右。小凯感觉很奇怪,然后登录线上的mysql,执行了查询命令,发现执行完成后耗时11s,奇了怪了,小凯又执行了几次,发现线上的响应时间确实很慢,小凯不知道怎么办了。
3.分析sql
小凯向同事小侯求助,侯哥,我这里有个问题,为啥线上的数据库搜索这么慢呢?侯哥说,你有没有尝试过用explain去分析,看看sql有没有走全表扫描。
小凯一拍脑袋,哎呀,咋忘了可以用explain来分析sql,之前的面试题都背到哪里去了。
小凯执行了explain,这是他执行得到的结果
线上环境

本地环境

小凯发现确实不一样,其中最大的区别在第三行
3.1 type 字段
type为All就是全表查询,type为ref就是普通索引查询 (其余的类型就请自行查阅了)
3.2 key + key_len + ref 字段
key代表是否用索引
key_len代表用索引的长度
ref代表MySQL在查找索引时,使用了哪些列或常量来与索引进行比较。
根据这三个字段,可以发现某一条sql本地用了索引,但是线上没有用索引。
3.3 rows 字段
rows 代表mysql所需要匹配的行数,也可以用来验证是否使用索引,这里发现线上要扫描17w行,而本地只要扫描50行,
4.尝试解决
根据explain分析,好像是线上的sql没有用到索引,但是本地用到了索引,会不会是线上的数据库没有添加索引的原因呢?
小凯登录了服务器,查看相关表的索引添加情况,发现线上和本地的索引添加完全相同,好像没有思路了。
5.真正解决
既然和索引的添加没有关系,那就从sql本身去分析,不考虑线上和本地的差异,看看这个sql还能不能优化。
这里把sql放出来,基本结构如下,简化了大部分的查询参数和排序和分组情况。实际上的sql比这个长很多,其中的一些敏感字段做了隐藏处理。
sql
SELECT
sub.process_name
FROM
(
SELECT
rsi.process_name
FROM
table1 AS rsi
LEFT JOIN table3 AS pl ON pl.procedur_name = rsi.process_name
WHERE
rsi.execution_mark = '1'
) AS sub
LEFT JOIN table2 AS rsif ON ( sub.production_order_number = rsif.production_order_number )
WHERE
sub.order_number IN ('xxx1', 'xxx2' )
sql分析:实际上就是一个查询作为子表,然后把子表连接到另一张表上,最后汇总得到查询结果。
有问题的sql片段
sql
SELECT
rsi.process_name
FROM
table1 AS rsi
LEFT JOIN table3 AS pl ON pl.procedur_name = rsi.process_name
WHERE
rsi.execution_mark = '1'
写sql多的人可能已经发现问题了,子查询是有问题的。
这个查询会把全部的table1的数据一起查出来,仅靠execution_mark是排除不了多少数据的。如果table1的行数很多,那这个大数据的表去联表,然后作为大数据量的子表去连接,这个时间肯定会消耗很多,唯一用来查询的order_number被放在了最外面。
好的,发现问题,开始优化,把最后的查询order_number字段放到子查询就可以了。
优化完的sql如下
sql
SELECT
sub.process_name
FROM
(
SELECT
rsi.process_name
FROM
table1 AS rsi
LEFT JOIN table3 AS pl ON pl.procedur_name = rsi.process_name
WHERE
rsi.execution_mark = '1' and rsi.order_number IN ('xxx1', 'xxx2' )
) AS sub
LEFT JOIN table2 AS rsif ON ( sub.production_order_number = rsif.production_order_number )
这样然后在线上执行以下explain分析

发现这个执行计划和本地的sql执行计划差不多了。
6.思考原因与总结
1.不同环境的差别
线上已经优化成和本地的速度差不多了。那说明本地的mysql自动帮我们优化了。本地的mysql版本是8.0,线上的mysql版本是5.7,mysql优化器应该检测到子查询后的条件可以直接放到里面,会节约查询时间,然后就对查询本身优化了,而mysql5.7没优化就导致查询慢了。
2.对sql优化不熟练 这算是个很常见的sql了,虽然是拿其他同事的sql拿来用,但是没仔细检查,很简单的优化点忘了。