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_typeAND 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語句更容易書寫和便於理解,任何查詢方式都有其適用場景,存在即合理。
MySQL Execution Plan--NOT EXISTS子查詢優化