表連線join查詢與where後使用子查詢的效能分析
宣告:本文摘自https://www.cnblogs.com/cdf-opensource-007/p/6540521.html
子查詢就是在一條查詢語句中還有其它的查詢語句,主查詢得到的結果依賴於子查詢的結果。
子查詢的子語句可以在一條sql語句的FROM,JOIN,和WHERE後面,本文主要針對在WHERE後面使用子查詢與表連線查詢的效能做出一點分析。
對於表連線查詢和子查詢效能的討論眾說紛紜,普遍認為的是表連線查詢的效能要高於子查詢。本文將從實驗的角度,對這兩種查詢的效能做出驗證,並就實驗結果分析兩種查詢手段的執行流程對效能的影響。
首先準備兩張表
1,訪問日誌表mm_log有150829條記錄(相關sql檔案已放在文章結尾的連結中)。
2,使用者表mm_member有373條記錄(相關sql檔案已放在文章結尾的連結中)。
現在要求我們根據這兩張表查出2017-02-06那天有那些使用者登入過系統。
我們先來看一下使用表連線查詢
SELECT SQL_NO_CACHE mm.* FROM mm_member mm JOIN mm_log ml ON mm.id = ml.member_id WHERE ml.access_time LIKE '%2017-02-06%' GROUP BY ml.member_id;
這裡使用了 SQL_NO_CACHE 是因為要多次執行這條sql語句,並計算出這條sql查詢所耗費的平均時間,所以要關掉mysql的查詢快取,防止多次執行從快取中讀取資料。
mm.*是取GROUP BY ml.member_id分組後的諸多臨時表的第一行資料,相關用法及原理請參見我的另一篇部落格(http://www.cnblogs.com/cdf-opensource-007/p/6502556.html)
對這條sql語句執行了10次,查詢所耗費的平均時間在0.120s左右。
查詢結果:(一共有5個使用者訪問過系統)
至於以上這條sql的執行流程已經在前幾篇部落格中描述的很詳細了,這裡就不再做敘述了。
下面使用WHERE後使用子查詢的方式實現
SELECT SQL_NO_CACHE mm.username FROM mm_member mm WHERE mm.id IN(SELECT ml.member_id FROM mm_log ml WHERE ml.access_time LIKE '%2017-02-06%' GROUP BY ml.member_id);
當我第一次執行這條sql語句的時候,等了十幾秒一直沒有結果,我以為我的電腦宕機,可Navicat顯示處理中,最後40多秒的時候才執行出結果,接連運行了好多次在都是41秒左右出結果。
我們看到執行結果同上。那麼使用子查詢的效能到底低在哪裡呢?
我的第一種推測是子語句:SELECT ml.member_id FROM mm_log ml WHERE ml.access_time LIKE '%2017-02-06%' GROUP BY ml.member_id耗費了大量的查詢時間,因為mm_log這張表中有150829條記錄。
把子語句單拿出來執行一下,發現子語句的執行時間也就在0.111s左右。
SELECT SQL_NO_CACHE member_id FROM mm_log ml WHERE ml.access_time LIKE '%2017-02-06%' GROUP BY ml.member_id;
這就說明我的第一種推測是不合理的。
那就分析下在WHERE後使用子查詢IN的執行原理:
1,IN後面跟的子查詢語句的執行結果只能有一列是用來和IN前面的主表的欄位匹配的,在這裡指的是mm.id。
2,一條帶有子查詢的sql語句首先執行的是子語句,主表的資料行按照IN前面主表的欄位依次跟子查詢的結果進行匹配,子查詢中結果中有該資料行對應欄位的值,則返回true,該行被WHERE篩選出來。沒有則返回false,該行不被篩選。
3,那麼按照2的說法,子查詢的效率應該也不低啊,子語句的耗時在0.111s左右,而且主表mm_member和子語句的查詢結果相匹配的次數,肯定是要少於表連線查詢時資料行間匹配的次數的,但實驗結果顯示使用子查詢的效能確實很低。
所以我有了第二種推測,主表mm_member資料行的每一行在與IN後面子語句的結果相匹配時,子語句都會重新執行一次,也就是說子語句第一次執行時,不會在記憶體中有快取。這類似與使用了兩個FOR迴圈巢狀,外層的FOR迴圈每拿出一個值,內層的FOR迴圈都要遍歷一次。
那麼根據以上的推測,拿主表mm_member的資料行數乘以子語句的執行時間就應該是整個查詢的時間。
mm_member的資料行數:373
多次執行子語句算出平均時間在:0.111s
整個查詢耗時的理論時間:41.403s
多次執行整個查詢得出實際查詢時間的平均值:40.834s
計算誤差:(理論值-實際值)÷理論值 = 1.37%
誤差還是在可以接受的範圍內的,可以證明以上的推測。
根據以上的實驗,我們可以得出的結論是,表連線查詢的效能是要高於子查詢的。
另外,對於在子查詢中使用IN的效能高還是是用EXITS的效能高,有一種普遍的說法是:
1,在外表大,內表小,外表中有索引的情況下,使用IN。
2,在外表小,內表大,內表中有索引的情況下,使用EXITS。
先介紹一下EXITS的用法,剛好本例符合外表小內表大的情況,就以本例介紹一下。看下SQL:
SELECT SQL_NO_CACHE mm.* FROM mm_member mm WHERE EXISTS(SELECT * FROM mm_log ml WHERE mm.id = ml.member_id AND ml.access_time LIKE '%2017-02-06%');
EXITS又簡稱代入查詢,就是把主表的每一行代入子表的每一行進行檢驗,一旦子表中有符合的資料行就返回true,就可以取得主表中代入檢驗的那一行資料,否則返回false,不可以取得主表中代入檢驗的那一行資料。同IN不同的是,EXITS後的子語句不查詢出結果,所以說SELECT後面的欄位沒有意義,一般使用*代替,由於EXITS的這種機制,當子表資料量比較大且有冗餘欄位的時候就很有可能避免了對子表的全表掃描,不像IN那樣每次主表資料行來匹配都要進行全表掃描,並返回結果。所以說EXITS類似於兩個FOR迴圈巢狀時,內層的FOR迴圈裡面有 if(xxx){ break; }這種語法。
以上sql執行時間的平均在34秒左右,比使用IN要快上一些,但是跟表連線查詢還不能比。
但是,在表與表之間沒有關聯關係時,就只能使用IN了。