MySQL Execution Plan--NOT EXISTS子查询优化

在很多业务场景中,会使用NOT EXISTS语句来确保返回数据不存在于特定集合,部分场景下NOT EXISTS语句性能较差,网上甚至存在谣言"NOT EXISTS无法走索引"。

首先需要明确的是:索引不是万能的,使用索引的执行计划并不一定就是最好的执行计划。

以某监控平台为例,使用NOT EXISTS的SQL为:

SELECT count(1)
FROM t_monitor m
WHERE NOT exists
(
    SELECT 1
    FROM t_alarm_realtime AS a
    WHERE a.resource_id=m.resource_id
    AND a.resource_type=m.resource_type
    AND a.monitor_name=m.monitor_name
)

该SQL执行时间为29秒,其执行计划为:

可以发现,上面SQL中使用到索引,但是表t_monitor上影响行数较高(578436),相当于遍历表t_monitor上索引idx_id_name_type上所有数据。

对于NOT EXISTS语句,常用的优化手段之一就是将NOT EXIST语句转换为LEFT JOIN语句,如将上面的SQL转换为:

SELECT count(1)
FROM t_monitor m
LEFT JOIN t_alarm_realtime AS a
ON a.resource_id=m.resource_id
AND a.resource_type=m.resource_type
AND a.monitor_name=m.monitor_name
WHERE a.resource_id is NULL

PS1:将NOT EXISTS转换为LEFT JOIN时,需要确定两表在关联条件上的数据处于1:1或1:0,否则需要在JOIN前或JOIN后对数据进行去重操作(DISTINCT)。

改写后SQL执行时间为1.2秒,性能提升约25倍,改写后执行计划为:

粗略对比两个执行计划,会发现相似度很高,使用的索引页相同,仅仅是select_type和Extra两列存在差异

两个执行计划差异:NOT EXISTS语句使用"DEPENDENT SUBQUERY",而LEFT JOIN使用SIMPLE方式。

为什么两者会有如此大差距呢?可以通过MySQL提供的Profiling方式来查看两种方式的执行过程。

使用NOT EXIST方式的执行过程:

使用LEFT JOIN方式的执行过程:

从执行过程来看,LEFT JOIN方式的主要消耗在Sending data一项上(1.2s),而NOT EXISTS方式主要消耗在executeing和Sending data两项上,受限于Profiling只存放100行记录缘故,从Profiling中只能看到47个” executeing和Sending data”的组合项(每个组合项约50us),通过执行计划看出,外表t_monitor的数据量为578436行,忽略统计信息不准情况下,使用NOT EXISTS方式应该会产生578436个” executeing和Sending data”的组合项,总计消耗时间=50μs*578436=28921800us=28.92s。

PS2:在MySQL 5.5版本中,使用Profiling查看执行过程会返回所有的步骤,而MySQL 5.7版本中,将步骤数量控制在100行以内,避免展示过多重复步骤影响查看。

问题总结:

对于NOT EXISTS方式语句,其执行性能严重依赖于子查询的循环执行次数,即外层查询结果集的数据量:

1、当外层查询结果集的数据量N较小时执行性能较好,如有N=10执行时间为50μs*10=500us=0.005s,再加上一些额外消耗,执行结果也能在0.01秒或10毫秒内范围,这个响应时间应该能被大部分应用程序接受。

2、当外层程勋结果集的数据量N较大甚至上千万数据量时,NOT EXISTS的查询性能会变得非常糟糕,甚至会大量消耗服务器IO和CPU资源从而影响其他业务正常运行。

NOT EXISTS语句相对于LEFT JOIN语句更容易书写和便于理解,任何查询方式都有其适用场景,存在即合理。

猜你喜欢

转载自www.cnblogs.com/gaogao67/p/10721052.html
今日推荐