Mysql改写子查询SQL优化案例

sql逻辑需求:需要定期统计表单数据,然后把汇总的结果展示在前端界面

根据业务逻辑实现了sql编写,产生了慢SQL
SELECT DISTINCT DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') as signDate,
                count(sr.PRODUCT_NO) as totalSign,
                (SELECT count(1)
                   FROM t_red_data t1
                  WHERE DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') =
                        DATE_FORMAT(t1.SIGN_DATE, '%Y-%m-%d')
                    AND t1.SIGN_TYPE = '1') AS sign,
                (SELECT count(1)
                   FROM t_red_data t2
                  WHERE DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') =
                        DATE_FORMAT(t2.SIGN_DATE, '%Y-%m-%d')
                    AND t2.SIGN_TYPE = '2') AS supplySign
  FROM t_red_data sr 
 WHERE sr.SIGN_DATE BETWEEN '2020-12-20 00:00:00' AND '2021-01-06 23:59:59'
 GROUP BY DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') LIMIT 0, 10;
 --耗费时间
 10 rows in set (15.32 sec)
上面这条sql 的执行时间用了15.32秒,下面来看看慢在哪儿?

执行计划:


这sql执行计划挺差的,虽然where条件列上有索引IDX_SIGN_DATE,但数据库CBO并没有选择,走了全表扫描,因为时间范围选的太大,CBO估算后发现走完索引再回表查数据,代价太高。

下面让该sql强制走索引IDX_SIGN_DATE 看看效果:

SELECT DISTINCT DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') as signDate,
                count(sr.PRODUCT_NO) as totalSign,
                (SELECT count(1)
                   FROM t_red_data t1
                  WHERE DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') =
                        DATE_FORMAT(t1.SIGN_DATE, '%Y-%m-%d')
                    AND t1.SIGN_TYPE = '1') AS sign,
                (SELECT count(1)
                   FROM t_red_data t2
                  WHERE DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') =
                        DATE_FORMAT(t2.SIGN_DATE, '%Y-%m-%d')
                    AND t2.SIGN_TYPE = '2') AS supplySign
  FROM t_red_data sr force index (IDX_SIGN_DATE)
 WHERE sr.SIGN_DATE BETWEEN '2020-12-20 00:00:00' AND '2021-01-06 23:59:59'
 GROUP BY DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') LIMIT 0, 10;
 --耗时时间更长了
 10 rows in set (15.94 sec)
强制索引后执行计划:


执行慢的原因总结:CBO选择的执行计划是对的,因为走索引后代价更高,sql更慢。耗时主要是出现在DEPENDENT SUBQUERY上,根据where条件相当于把该表执行了三遍。主要解决子查询的问题。

通过表自关联消除列上子查询
SELECT signDate, totalSign, sign, totalSign - sign AS supplySign
  FROM (SELECT DISTINCT DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') AS signDate,
                        count(sr.PRODUCT_NO) AS totalSign,
                        count(t1.ID) AS sign
          FROM t_red_data sr
          LEFT JOIN t_red_data t1
            ON sr.id = t1.id
           AND t1.SIGN_TYPE = '1'
         WHERE sr.SIGN_DATE BETWEEN '2020-12-20 00:00:00' AND
               '2021-01-06 23:59:59'
         GROUP BY DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') LIMIT 0, 10) t;
     
 --耗时时间
 10 rows in set (1.75 sec)
改为自关联查询后就快了很多,接下来看下执行计划:


从执行计划可以看出,sr表作为驱动表,根据where条件筛选数据后拿到id再去驱动t1表,按条件匹配到10条数据后就结束,效率看起来不错...

但是还没结束,仅仅是统计下不同条件上的数据量,真的有必要自关联查询么?

自关联改写为case when


SELECT DISTINCT DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') as signDate,
                count(sr.PRODUCT_NO) as totalSign,
                count(case when sr.SIGN_TYPE = '1' then '1' else null end ) AS sign,
                count(case when sr.SIGN_TYPE = '2' then '2' else null end ) AS supplySign
  FROM t_red_data sr
 WHERE sr.SIGN_DATE BETWEEN '2020-12-20 00:00:00' AND '2021-01-06 23:59:59'
 GROUP BY DATE_FORMAT(sr.SIGN_DATE, '%Y-%m-%d') LIMIT 0, 10;
 
 --执行耗时
 10 rows in set (0.75 sec)
执行效率又进一步提升了,再次开口执行计划发生了啥改变:


该sql也变得更简单了,做了按where条件过滤数据后再把group by的排序下,相对之前的sql少做了不少事情,效率自然就高。另外,已经有了group by字句,本身已经消除了重复数据,所以前面的distinct关键词可以去掉了。

使用redis缓存查询结果
上面的sql 执行时间0.75秒还是不满足。 和业务沟通确认,这张表上1天前的数据不会改动,也就是根据时间条件多次执行这条sql结果不会发生变化。

把当天第一次查询出来的结果集缓存到redis,当sql查询日期条件改变时把缓存失效掉。这样就可以查询一次多次使用,访问redis效率更高,毫秒级返回。

猜你喜欢

转载自blog.csdn.net/u010033674/article/details/112994997