子查詢就是在一條查詢語句中還有其它的查詢語句,主查詢得到的結果依賴於子查詢的結果。
子查詢的子語句可以在一條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了。
sql 文件位置:http://pan.baidu.com/s/1gfLwIwr
最后說一點,我們作為程序員,研究問題還是要仔細深入一點的。當你對原理了解的有夠透徹,開發起來也就得心應手了,很多開發中的問題和疑惑也就迎刃而解了,而且在面對其他問題的時候也可做到觸類旁通。當然在開發中沒有太多的時間讓你去研究原理,開發中要以實現功能為前提,可等項目上線的后,你有大把的時間或者空余的時間,你大可去刨根問底,深入的去研究一項技術,為覺得這對一名程序員的成長是很重要的事情。