> 文章列表 > 生产上的一次慢查询SQL优化

生产上的一次慢查询SQL优化

生产上的一次慢查询SQL优化

一、背景

MySQL版本5.6.16,InnoDB
生产上有个业务场景,需要每日拉取还款计划表里某些产品编号的所有状态为0,1数据的借据号和产品编号,SQL如下

select distinctloan_no,product_codefrom 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 ascLIMIT #{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

生产上的一次慢查询SQL优化
很明显查了全表,一次SELECT耗时700+s,rds监控也印证了问题,在执行时间内打满了IO
生产上的一次慢查询SQL优化
在生产环境Explain分析SQL执行计划
生产上的一次慢查询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_codeFROM repay_planINNER JOIN (SELECT idFROM repay_plan force index (idx_gmt_created)<where>status in ('0','1')and gmt_created &gt;=#{condition.startDate,jdbcType=TIMESTAMP}and gmt_created &lt;=#{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 ascLIMIT #{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条记录的主键索引去读取数据块。