暂无图片
暂无图片
4
暂无图片
暂无图片
暂无图片

MySQL Where条件写重复导致的慢查询

原创 chengang 2025-06-11
341

1、问题复现

开发同学发来一个SQL,说不能理解为什么执行计划选择全表扫描,驱动表也选择错误,原始SQL更复杂,我将SQL简化后如下

explain select * from erp_bill_sale ebi inner join erp_bill_index outbill ON outbill.orderbillid=ebi.billid AND outbill.billtype=601 AND outbill.ifred=0 AND ebi.profileid=outbill.profileid and outbill.status in(20,28) where ebi.profileid=200006732 AND ebi.billtype IN (604) and ebi.billdate>='2025-05-26' and ebi.billdate<'2025-06-01' and outbill.status in(20,28)

image.png
通过执行计划可以看到 MySQL选用了outbill做驱动表,而且是全表扫描

我写个hint强制用ebi做驱动表看看计划

explain select /*+ JOIN_PREFIX(EBI) */ * from erp_bill_sale ebi inner join erp_bill_index outbill ON outbill.orderbillid=ebi.billid AND outbill.billtype=601 AND outbill.ifred=0 AND ebi.profileid=outbill.profileid and outbill.status in(20,28) where ebi.profileid=200006732 AND ebi.billtype IN (604) and ebi.billdate>='2025-05-26' and ebi.billdate<'2025-06-01' and outbill.status in(20,28)

image.png
真正的执行时间也相差100倍左右,outbill做驱动表需要60多秒,而ebi做驱动表只需要0.63秒

这里明显该用ebi做驱动表,但为什么MySQL选用了outbill呢?
我们慢慢的来找原因。

2、MySQL优化器改进建议

以下的知识点最终会影响上面执行计划的错误原因

MySQL官网有对where子句优化的说明
常见的有常数条件去除/常量折叠,删除不必要的括号等
在官网有现成的例子
比如有 where (a<b AND b=c) AND a=5 这样的条件,会优化成where b>5 AND b=c AND a=5
where (b>=5 AND b=5) OR (b=6 AND 5=5) OR (b=7 AND 5=6) => where b=5 OR b=6
详见官网

即MySQL通过优化器或准备阶段,会改写你的SQL,你可以通过show warnings 来查看优化器改写后的SQL

而在上面语句中有两个完全重复的条件 即outbill.status in(20,28) 写了两次在inner join 的on 条件写了一次,又在where条件处又写了一次。正因为多写了一次改变了整个查询计划

我们单独将Outbill表单独摘出来查询一次

explain select * from erp_bill_index outbill where outbill.profileid=200006732 AND outbill.billtype IN (601) AND outbill.ifred=0 and outbill.status in(20,28) and outbill.status in(20,28) ;

image.png
通过执行计划得知该查询计划的filtered = 0.02

我们去掉一个多余的 and outbill.status in(20,28) 再来观察执行计划的filtered值

explain select * from erp_bill_index outbill where outbill.profileid=200006732 AND outbill.billtype IN (601) AND outbill.ifred=0 and outbill.status in(20,28) -- and outbill.status in(20,28) ;

image.png

通过执行计划可以看到filtered的值由0.02变成了0.1

但由于是完全重复的多余条件,实际不会对结果产生影响,但MySQL估算filtered还是变化了,当是等值条件写重复的时候,MySQL优化器会直接干掉重复的条件
举一个例子

explain select 1 from erp_bill_index outbill where outbill.profileid=200006732 and outbill.billtype= 601 AND outbill.billtype= 601

通过show warnings 可以看到
语句变成了以下

/* select#1 */ select 1 AS `1` from `erp_ql_539`.`erp_bill_index` `outbill` where ((`erp_ql_539`.`outbill`.`billtype` = 601) and (`erp_ql_539`.`outbill`.`profileid` = 200006732))

重复的outbill.billtype = 601 被消除掉了,所以无论如何重复写都不会让执行计划的filtered变化

explain select 1 from erp_bill_index outbill where outbill.profileid=200006732 and outbill.ifred in (0,1) and outbill.ifred in (0,1)

通过show warnings 可以看到
语句变成了以下

/* select#1 */ select 1 AS `1` from `erp_ql_539`.`erp_bill_index` `outbill` where ((`erp_ql_539`.`outbill`.`profileid` = 200006732) and (`erp_ql_539`.`outbill`.`ifred` in (0,1)) and (`erp_ql_539`.`outbill`.`ifred` in (0,1)))

重复的and outbill.ifred in (0,1) 没有被消除

建议MySQL可以优化一下。重复的in条件 也可以消除,不要影响filtered值

因为filtered 会影响多表关联驱动表的选择
参见官网详细文档

3、问题解决

回到此问题本身上来

explain select * from erp_bill_sale ebi inner join erp_bill_index outbill ON outbill.orderbillid=ebi.billid AND outbill.billtype=601 AND outbill.ifred=0 AND ebi.profileid=outbill.profileid and outbill.status in(20,28) where ebi.profileid=200006732 AND ebi.billtype IN (604) and ebi.billdate>='2025-05-26' and ebi.billdate<'2025-06-01' -- and outbill.status in(20,28)

image.png
通过执行计划可以看到只要注释掉多余的and outbill.status in(20,28) 即走了正确的执行计划

如果你认真看了第2点链接中官网对filtered的详细介绍,你可能已经知晓答案了
即因为多了一个in条件,导致 filtered很小了,MySQL认为,表Outbill通过filtered剩下的行将很少了,更适合做驱动表。刚好多一个 in 与少一个in 达到了驱动表评估的临界值。

我们可以通过优化器开关来确认这个推测

explain select /*+ set_var(optimizer_switch='condition_fanout_filter=off') */ * from erp_bill_sale ebi inner join erp_bill_index outbill ON outbill.orderbillid=ebi.billid AND outbill.billtype=601 AND outbill.ifred=0 AND ebi.profileid=outbill.profileid and outbill.status in(20,28) where ebi.profileid=200006732 AND ebi.billtype IN (604) and ebi.billdate>='2025-05-26' and ebi.billdate<'2025-06-01' and outbill.status in(20,28)

image.png
通过执行计划可以看出,即使多了一个and outbill.status in(20,28) 驱动表选择也是正确的

因为condition_fanout_filter=off 代表filtered值不影响多表join驱动表的选择

至此,此语句为什么选错驱动表的原因已找到

开发写SQL,一定得慎重,小心,有可能你认为无任何影响的写法,却导致生产事故的发生,上述语句,在我的环境中,一个只需要0.63秒,一个需要60多秒

最后修改时间:2025-06-12 09:44:52
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

文章被以下合辑收录

评论