引言
本文用于记录作者在一个后台管理系统项目中,遇到的页面查询未走索引导致慢SQL问题,以及优化过程。
问题现象
如下图,某个IoT系统的后台管理页面,查询已绑定的设备列表,20万设备量级,耗时12.64s

性能分析
从业务上看,这只是一个后台查询页面,并无复杂的业务逻辑,因此作者凭经验判断大概率是SQL问题。
直接抓取SQL。抓取SQL有多种方式,如果你有MySQL数据库权限,
8.0以下直接通过客户端工具执行SHOW full PROCESSLIST ;,8.0以上版本执行SELECT * FROM performance_schema.processlist WHERE COMMAND IS NOT NULL;
如果没有数据库权限,看日志、获取使用其他工具如arthas等也可以抓到SQL。
如下图,这种明显比较慢的SQL一眼就能看出来

抓取到SQL如下
select
d.id as did,
d.model_id,
d.uid,
d.device_name,
d.imei,
IFNULL(d.area_id, 0) as areaId,
d.bind_time,
d.dealer_name,
dm.model_name,
dm.type_id as tid,
sc.iccid,
di.software,
u.user_email,
u.uuid,
dm.app_type AS appTypeId,
u.login_time,
il.create_time as latestPicTime,
pi.name AS planName,
pi.inapp_id AS planCode,
dp.id AS planId,
CASE
WHEN dp.pay_id IS NULL THEN 1
WHEN dp.pay_id = 0 THEN 1
ELSE 0
END AS isGive
from device.device d
left join app.`user` u on u.id = d.uid and u.del_status = 0
left join device.device_sim ds on ds.did = d.id and ds.`status` = 1
left join device.sim_card sc on sc.id = ds.sim_id
left join device.device_model dm on dm.id = d.model_id
left join device.device_info di on di.did = d.id
left join app.device_plan dp on dp.did = d.id and dp.del_status = 0
left join app.plan_item pi on pi.inapp_id = dp.inapp_id
left join device.image_latest il on il.did = d.id
WHERE d.del_status = 0
and d.area_id = 1
and dm.app_type = 10
ORDER BY d.id desc
LIMIT 0, 10SQL优化
通用的优化思路,一看业务实现,二看执行计划。
业务实现
业务逻辑上,这是一个设备列表查询,查询用户已绑定未删除的、绑了SIM卡的、型号存在的、设备明细信息存在的、设备套餐存在的设备信息。业务上暂时看不出问题。
执行计划
查看执行计划,根据执行计划分析慢sql原因
如下图,明显看到执行顺序是dm、d、u······从上往下顺序执行,但从SQL语句分析,执行顺序应该是首先查d,再根据d匹配其他表,
且dm表执行时用到文件排序和临时表,

观察SQL语句,查询条件left join device.device_model dm on dm.id = d.model_id中用到dm的仅dm.id,possible_keys也显示了primary,但是执行计划未走primary主键索引,而是all全表扫
表索引如下,明显本应该走primary主键id索引

结合具体表数量,仅4条,因此确定是表数据量较少时,mysql优化器认为走all比走索引更快,因此导致了执行时用文件排序和临时表

优化措施,考虑最小的改动达到最大的性能,由于这是默认的优化器走了all,通常这类情况,我们可以增加强制索引,强制走id索引。
成果检验
如下图,新增强制索引后耗时从12s降到343ms,

经验总结
后台的查询业务,页面查询慢,由于无复杂的业务逻辑,仅仅只是查询,95%的原因都是SQL慢,一般通过如下步骤调优。
1)抓取慢SQL
2)判断业务逻辑合理性
3)判断执行计划合理性
4)执行计划不合理,通过增减索引、强制索引等方式进行调优