一、背景
MySQL版本5.6.16,InnoDB
生产上有个业务场景,需要每日拉取还款计划表里某些产品编号的所有状态为0,1数据的借据号和产品编号,SQL如下
select distinct
loan_no,product_code
from repay_plan
<where>
status in ('0','1') --状态
and gmt_created >=#{condition.startDate,jdbcType=TIMESTAMP} --创建时间
and gmt_created <=#{condition.endDate,jdbcType=TIMESTAMP}
and product_code in
<foreach item="productCode" collection="condition.productCodes" index="index" open="(" separator=","
close=")">
#{productCode,jdbcType=VARCHAR} --产品编号
</foreach>
</where>
order by gmt_created asc
LIMIT #{condition.offset}, #{condition.limit}
repay_plan单表,数据量1500w,已有普通索引
KEY `idx_product_code` (`product_code`) USING BTREE,
KEY `idx_gmt_created` (`gmt_created`) USING BTREE,
product_code是一个枚举,SQL中in条件几乎包含了所有的枚举值
上线后发现任务执行缓慢
二、问题排查
通过日志观察发现大概率是SQL慢查询问题,进行慢查询追踪,成功找到这条SQL
很明显查了全表,一次SELECT耗时700+s,rds监控也印证了问题,在执行时间内打满了IO
在生产环境Explain分析SQL执行计划
发现SQL优化器在product_code和gmt_created两个索引中选择了product_code,但我们知道product_code中包含了几乎所有枚举值,此时选择走product_code与不使用索引无异。应使用gmt_created索引。
在测试环境中模拟生产环境,脚本随机生成80w条数据,执行explain和sql,问题复现。
三、优化
到此我们已经清楚SQL慢查询发生的原因:
1.SQL优化器索引选择错误,导致扫描全表
2.limit的offset过大导致IO效率低下,IO被打满
优化后SQL如下,通过强制指定索引减少扫描行数,使用主键索引访问数据块提升IO效率
SELECT DISTINCT loan_no, product_code
FROM repay_plan
INNER JOIN (
SELECT id
FROM repay_plan force index (idx_gmt_created)
<where>
status in ('0','1')
and gmt_created >=#{condition.startDate,jdbcType=TIMESTAMP}
and gmt_created <=#{condition.endDate,jdbcType=TIMESTAMP}
and product_code in
<foreach item="productCode" collection="condition.productCodes" index="index" open="(" separator=","
close=")">
#{productCode,jdbcType=VARCHAR}
</foreach>
</where>
order by gmt_created asc
LIMIT #{condition.offset}, #{condition.limit}
) `tmp_0` USING (`id`)
优化后观察监控,SQL执行时间降低至1s内,IO使用率正常
四、总结
1.MySQL优化器虽然这次出了问题,但它依然值得信赖,非特殊情况不要强制指定SQL优化,MySQL优化器比我们更懂SQL。
2.由于InnoDB索引的特性,使用limit要注意offset过大导致的性能问题,即offset过大,多次通过主键索引访问数据块的I/O操作。解决办法是在找到主键索引后,先执行offset偏移处理,跳过{offset}条,再通过第{offset}+1条记录的主键索引去读取数据块。