案例 4:MySQL 优化:为什么 SQL 走索引还那么慢?
作者:王航威
1 背景
这个问题是一个朋友遇到的@风云,并且这位朋友已经得出了近乎正确的判断,下面进行一些描述。
2019-01-11 9:00-10:00 一个 MySQL 数据库把CPU 打满了。
硬件配置:256G 内存,48core
2 分析过程
接手这个问题时现场已经不在了,信息有限,所以我们先从监控系统中查看一下当时的状态。从PMM 监
控来看,这个MySQL 实例每天上午九点CPU 都会升高到10%-20%,只有1 月2 号 和1 月11 号CPU 达到
100%,也就是今天的故障。怀疑是业务在九点会有压力下发,排查方向是慢查询。
2.1 按执行次数统计slow log 发现次数最多的一条sql:
mysqldumpslow -s c slow.log>/tmp/slow_report.txt
Count: 3276 Time=21.75s (71261s) Lock=0.00s (1s) Rows=0.9 (2785), xxx
SELECT T.TASK_ID,
T.xx,
T.xx,
…
FROM T_xx_TASK T
WHERE N=N
AND T.STATUS IN (N,N,N)
AND IFNULL(T.MAX_OPEN_TIMES,N) > IFNULL(T.OPEN_TIMES,N)
AND (T.CLOSE_DATE IS NULL OR T.CLOSE_DATE >= SUBDATE(NOW(),INTERVAL ‘S’ MINUTE))
AND T.REL_DEVTYPE = N
AND T.REL_DEVID = N
AND T.TASK_DATE >= ‘S’
AND T.TASK_DATE <= ‘S’
ORDER BY TASK_ID DESC
LIMIT N,N
2.2 在slow log 中找到这条查询记录扫描行数:“Rows_examined: 1161559”,看起来是全表扫描,
CPU 升高通常原因就是同时执行大量慢sql,所以接下来分析这个sql3.

2.3 因为T_xxx_TASK 表在现场应急时清理过数据(从110 万删至4 万行),所以需要用备份恢复该表
到故障前。恢复备份后,查看执行计划与执行时间:
explain SELECT T.TASK_ID,
T.xx,
…
FROM T_xxx_TASK T
WHERE 1=1
AND T.STATUS IN (1,2,3)
AND IFNULL(T.MAX_OPEN_TIMES,0) > IFNULL(T.OPEN_TIMES,0)
AND (T.CLOSE_DATE IS NULL OR T.CLOSE_DATE >= SUBDATE(NOW(),INTERVAL ‘10’ MINUTE))
AND T.REL_DEVTYPE = 1
AND T.REL_DEVID = 000000025xxx
AND T.TASK_DATE >= ‘2019-01-11’
AND T.TASK_DATE <= ‘2019-01-11’
ORDER BY TASK_ID DESC
LIMIT 0,20;
1 row in set (10.37 sec)
执行时间 10s+,查看表索引信息:
show index from T_xxx_TASK;
看到这里其实已经可以基本确定是这个SQL 引起的了,因为执行一次就要10s+,而且那个时间点会并发
下发大量的这个SQL。但是有一点陷阱藏在这里:
- 执行计划中明明有使用到索引,为什么执行还是这么慢?
- 执行计划中显示扫描行数为644,为什么slow log 中显示100 多万行?
a. 我们先看执行计划,选择的索引 “INDX_BIOM_ELOCK_TASK3(TASK_ID)”。结合 sql 来看,因为有
“ORDER BY TASK_ID DESC” 子句,排序通常很慢,如果使用了文件排序性能会更差,优化器选择这个索
引避免了排序。
那为什么不选 possible_keys:INDX_BIOM_ELOCK_TASK 呢?原因也很简单,TASK_DATE 字段区分度太低
了,走这个索引需要扫描的行数很大,而且还要进行额外的排序,优化器综合判断代价更大,所以就不

选这个索引了。不过如果我们强制选择这个索引(用 force index 语法),会看到 SQL 执行速度更快少 于 10s,那是因为优化器基于代价的原则并不等价于执行速度的快慢; b. 再看执行计划中的 type:index,“index” 代表 “全索引扫描”,其实和全表扫描差不多,只是扫描 的时候是按照索引次序进行而不是行,主要优点就是避免了排序,但是开销仍然非常大。 Extra:Using where 也意味着扫描完索引后还需要回表进行筛选。一般来说,得保证 type 至少达到 range 级别,最好能达到 ref。在第 2 点中提到的“慢日志记录Rows_examined: 1161559,看起来是全 表扫描”,这里更正为“全索引扫描”,扫描行数确实等于表的行数;c. 关于执行计划中:“rows: 644”,其实这个只是估算值,并不准确,我们分析慢 SQL 时判断准确的扫描行数应该以 slow log 中的 Rows_examined 为准。 2.4 优化建议:添加组合索引 IDX_REL_DEVID_TASK_ID(REL_DEVID,TASK_ID) 3 优化过程 TASK_DATE 字段存在索引,但是选择度很低,优化器不会走这个索引,建议后续可以删除这个索引: select count(),count(distinct TASK_DATE) from T_BIOMA_ELOCK_TASK; +————+—————————+ | count() | count(distinct TASK_DATE) | +————+—————————+ | 1161559 | 223 | +————+—————————+ 在这个sql 中 REL_DEVID 字段从命名上看选择度较高,通过下面sql 来检验确实如此: select count(),count(distinct REL_DEVID) from T_BIOMA_ELOCK_TASK; +———-+—————————+ | count() | count(distinct REL_DEVID) | +———-+—————————+ | 1161559 | 62235 | +———-+—————————+ 由于有排序,所以得把 task_id 也加入到新建的索引中,REL_DEVID,task_id 组合选择度 100%: select count(),count(distinct REL_DEVID,task_id) from T_BIOMA_ELOCK_TASK; +———-+———————————–+ | count() | count(distinct REL_DEVID,task_id) | +———-+———————————–+ | 1161559 | 1161559 | +———-+———————————–+ 在测试环境添加REL_DEVID,TASK_ID 组合索引,测试sql 性能:alter table T_BIOMA_ELOCK_TASK add index idx_REL_DEVID_TASK_ID(REL_DEVID,TASK_ID);
添加索引后执行计划:
这里还要注意一点“隐式转换”:REL_DEVID 字段数据类型为varchar,需要在sql 中加引号:AND
T.REL_DEVID = 000000025xxx » AND T.REL_DEVID = ‘000000025xxx’
执行时间从 10s+ 降到毫秒级别:
1 row in set (0.00 sec)
4 结论
一个典型的order by 查询的优化,添加更合适的索引可以避免性能问题:执行计划使用索引并不意味着
就能执行快。
