一个bug引发的对mysql语句的探究

一个bug引发的对mysql语句的探究

mysql 子查询加limit 和不加limit区别居然这么大

问题描述

最近改了一个bug ,原有的系统是经过测试的,但是去年底负责这个系统的人离职了,然后环境切换后各种问题又爆粗来了,而手头目前没啥活儿的我就接过了锅,当然走的时候也不是和我交接的...废话不多说,说多了就是背锅侠了;我们还是来看问题:

测试反馈治理系统首页统计治理数据总量始终显示为0,对应业务是某个角色对应的部门下面对应的数据源中对应的表被清洗,而被清洗表的数据总量会汇总统计出来,现在问题是没统计到,提了bug挂在我名下;那能怎么办,可以肯定的是业务流程之前是没问题的(之前整个系统业务是测过一遍的),就需要扒代码看查询逻辑了;

梳理下对应的业务逻辑如下:登录用户->对应角色->对应部门->对应数据源->对应库->对应表->对应表被创建定时调度任务(xxl job)->调度任务触发对应的治理平台的执行器-> 数据进行清洗筛选入库

好了,整个流程就是酱紫;话说执行器里的锅是真的大,200多行的方法家常便饭,最长的有500多行的。。。看着头大,看了看手边的《重构改善既有代码的设计》,我特么重构个xx,这面向过程编码的代码看着都脑阔疼,还是不能抱怨,生活不易请善待自己;

问题定位

找到对应的接口,发现汇总的是一个查询出来的allNum字段,看看对应的查询语句:

SELECT
t.report_id,
t.meet_data + t.no_meet_data allNum
FROM
report_detail t
LEFT JOIN monitor_report t1 ON t1.id = t.report_id
WHERE
t1.data_state = 1
AND t.data_state = 1
GROUP BY
t.report_id
ORDER BY
t.create_date DESC

执行下看看查询结果:

我们修改下sql 加个日期,看看查询结果:

很明显,report_id为9的 数据总量是有为3的,至于为什么查出来多条,完全是执行器里面的逻辑,被调度一次就往表里塞一条数据导致的...还是就事论事,其实按这个sql 层面来理解,就是需要取出某个report_id 下最新时间点的那条数据。可惜这个sql写错了,应该先按时间倒排序 再按时间分组哈;先GROUP BY 会默认按时间正排序取出最小时间点的那条数据,不禁要问再按时间倒排序有何意义?

改写sql

按上面理解,改写sql 查询如下(ps:只是说明问题哈,当然sql中尽量避免使用子查询,应该是数据查询出来后逻辑筛选过滤,当然当前业务根本的解决的办法是去修改执行器里面的逻辑,存在历史数据即更新不存在则新增数据,这样才能保证一个report_id只有一条记录)

好,我们看下修改后查询结果:

wtf! 为毛report_id 为9的数据查出来了, 总量为3的没被查出来了,这个时间为毛都是最小时间呢?搞得我一度还以不是DESC 用错了,换成ASC试了下,还是一毛一样的;完蛋,借助搜索引擎来搞一波,说要加上limit 查询;那我们加上看下

思考

这是为什么呢,为啥加上limit查询条件就可以查到对应最大时间点的那条数据了咧?

我们还是来看下执行解释:

对比后发现执行解释中,加了limit 下面两个查询变成了DERIVED(被驱动的SELECT子查询)

关于select_type可以查看博客:mysql 查询优化 ~explain解读之select_type的解读

关于msql 执行计划的博客推荐:MySQL explain执行计划解读

所以,我们了解到加了limit 的本质是执行计划改变了,其实不加limit 默认子查询就是查一条数据,这时候select_type 为简单的SELECT语句,认为两个查询语句不是子查询关系;加了limit 关键字就变成子查询语句了;至于为啥变,这就涉及到底层的知识盲区了...哈哈,开始战略性后撤!

总结

看来,理不辨不明;还是要多思考,才能进步哈;回到问题本身,这里sql 还有没有其他写法呢?答案是肯定的,如下:

SELECT t1.report_id reportId,
    t1.meet_data + t1.no_meet_data allNum,
    t1.`create_date` AS createDate 
    FROM report_detail AS t1
    INNER JOIN(SELECT MAX(create_date) AS create_date FROM report_detail GROUP BY report_id) AS t2
    ON t1.create_date = t2.create_date
    LEFT JOIN monitor_report mr 
      ON mr.id = t1.report_id 
    WHERE mr.data_state = 1 
    AND t1.data_state = 1 
GROUP BY t1.report_id

先使用内连接,查询出最大日期作为条件,然后关联查询出最大时间的数据后,再使用左连接关联查询;

哈哈,简单测了下查询效率还不如子查询。因为内连接相当于report_detail这个表被遍历两遍!数据量大了就会暴露出问题来,所以还是交由逻辑处理好;

好了,由一个bug 引发的对sql执行解释的思考就先总结到这里,我又要去改其他的bug了...漫漫背锅路,加油吧少年!