今天遇到一個left join優化的問題,搞了一下午,中間查了很多資料,對MySQL的查詢計劃還有查詢優化有了更進一步的瞭解,作一個簡單的記錄:
select c.* from hotel_info_original c
left join hotel_info_collection h
on c.hotel_type=h.hotel_type and c.hotel_id =h.hotel_id
where h.hotel_id is null
這個sql是用來查詢出c表中有h表中無的記錄,因此想到了用left join的特性(返回左邊所有記錄,右表不知足匹配條件的記錄對應行返回null)來知足需求,不料這個查詢很是慢。先來看查詢計劃:
rows表明這個步驟相對上一步結果的每一行須要掃描的行數,能夠看到這個sql須要掃描的行數爲35773*8134,很是大的一個數字。原本c和h表的記錄條數分別爲40000+和10000+,這幾乎是兩個表作笛卡爾積的開銷了(select * from c,h)。
因而我上網查了下MySQL實現join的原理,原來MySQL內部採用了一種叫作 nested loop join的算法。Nested Loop Join 實際上就是經過驅動表的結果集做爲循環基礎數據,而後一條一條的經過該結果集中的數據做爲過濾條件到下一個表中查詢數據,而後合併結果。若是還有第三個參與 Join,則再經過前兩個表的 Join 結果集做爲循環基礎數據,再一次經過循環查詢條件到第三個表中查詢數據,如此往復,基本上MySQL採用的是最容易理解的算法來實現join。因此驅動表的選擇很是重要,驅動表的數據小能夠顯著下降掃描的行數。
那麼爲何通常狀況下join的效率要高於left join不少?不少人說不明白緣由,只人云亦云,我今天下午感悟出來了一點。通常狀況下參與聯合查詢的兩張表都會一大一小,若是是join,在沒有其餘過濾條件的狀況下MySQL會選擇小表做爲驅動表,可是left join通常用做大表去join小表,而left join自己的特性決定了MySQL會用大表去作驅動表,這樣下來效率就差了很多,若是我把上面那個sql改爲
select c.* from hotel_info_original c
join hotel_info_collection h
on c.hotel_type=h.hotel_type and c.hotel_id =h.hotel_id
查詢計劃以下:
很明顯,MySQL選擇了小表做爲驅動表,再配合(hotel_id,hotel_type)上的索引瞬間下降了好多個數量級。。。。。
另外,我今天還明白了一個關於left join 的通用法則,即:若是where條件中含有右表的非空條件(除開is null),則left join語句等同於join語句,可直接改寫成join語句。
後記:
隨着查看MySQL reference manual對這個問題進行了更進一步的瞭解。MySQL在執行join時會把join分爲system/const/eq_ref/ref/range/index/ALl等好幾類,鏈接的效率從前日後
依次遞減,對於個人第一個sql,鏈接類型是index,因此幾乎是全表掃描的效果。可是我很奇怪我在(hotel_id,hotel_type)兩列上聲明瞭unique key,根據官方文檔鏈接類型應該是eq_ref纔對,
這個問題一直困擾了我兩天,在google和stackoverflow上都沒有找到可以解釋這個問題的文章,莫非我這個問題無解了?抱着解決這個問題的決心今天又翻看了一遍MySQL官方文檔
關於優化查詢的部分,看到了這樣一句:這裏的一個問題是MySQL能更高效地在聲明具備相同類型和尺寸的列上使用索引。我感受我找到了問題所在,因而我將original和 collection表的(hotel_type,hotel_id)的encoding和collation(決定字符比較的規則)所有改爲統一的utf8_general_ci,而後再次運行第一條sql的查詢計劃,獲得以下結果:
鏈接類型已經由index優化到了ref,若是將hotel_type申明爲not null能夠優化到eq_ref,不過這裏影響不大了,優化後這條sql能在0.01ms內運行完。
那麼如何優化left join:
一、條件中儘可能可以過濾一些行將驅動表變得小一點,用小表去驅動大表
二、右表的條件列必定要加上索引(主鍵、惟一索引、前綴索引等),最好可以使type達到range及以上(ref,eq_ref,const,system)
三、無視以上兩點,通常不要用left join~~! 算法