連接查詢應該是比較常用的查詢方式,連接查詢大致分為:內連接、外連接(左連接和右連接)、自然連接
下圖展示了 LEFT JOIN、RIGHT JOIN、INNER JOIN、OUTER JOIN 相關的 7 種用法。
內連接
以下三種寫法都是內連接:
mysql> select * from t1 join t2 on t1.a = t2.a; mysql> select * from t1 inner join t2 on t1.a = t2.a; mysql> select * from t1 cross join t2 on t1.a = t2.a;
圖解:
左連接、右連接
左連接:
mysql> select * from t1 left join t2 on t1.a = t2.a;
右連接:
mysql> select * from t1 right join t2 on t1.a = t2.a;
連接的原理
不管是內連接還是左右連接,都需要一個驅動表和一個被驅動表,對於內連接來說,選取哪個表為驅動表都沒關系,而外連接的驅動表是固定的,也就是說左連接的驅動表就是左邊的那個表,右連接的驅動表就是右邊的那個表。
連接的大致原理是:
1. 選取驅動表,使用與驅動表相關的過濾條件,選取代價最低的訪問形式來執行對驅動表的單表查詢。
2. 對上一步驟中查詢驅動表得到的結果集中每一條記錄,都分別到被驅動表中查找匹配的記錄。
對應偽代碼就是:
for each row in t1 { //此處表示遍歷滿足對t1單表查詢結果集中的每一條記錄 for each row in t2 { //此處表示對於某條t1表的記錄來說,遍歷滿足對t2單表查詢結果集中的每一條記錄 // 判斷是否符合join條件 } }
嵌套循環連接(Nested-Loop Join)
上面的過程就像是一個嵌套的循環,所以這種驅動表只訪問一次,但被驅動表卻可能被多次訪問,訪問次數取決於對驅動表執行單表查詢后的結果集中的記錄條數的連接執行方式稱之為嵌套循環連接(Nested-Loop Join),這是最簡單,也是最笨拙的一種連接查詢算法;
比如對於下面這個sql:
select * from t1 join t2 on t1.a = t2.a where t1.b in (1,2);
先會執行:
mysql> select * from t1 where t1.b in (1,2); +---+------+------+------+------+ | a | b | c | d | e | +---+------+------+------+------+ | 1 | 1 | 1 | 1 | a | | 2 | 2 | 2 | 2 | b | | 5 | 2 | 3 | 5 | e | +---+------+------+------+------+ 3 rows in set (0.00 sec)
得到三條記錄。
然后分別執行:
mysql> select * from t2 where t2.a = 1; mysql> select * from t2 where t2.a = 2; mysql> select * from t2 where t2.a = 5;
所以實際上對於上面的步驟,實際上都是針對單表的查詢,所以都可以使用索引來幫助查詢。
基於塊的嵌套循環連接(Block Nested-Loop Join)
掃描一個表的過程其實是先把這個表從磁盤上加載到內存中,然后從內存中比較匹配條件是否滿足。現實生活中的表可不像t1、t2這種只有幾條記錄,可能會有成千上萬的數據。內存里可能並不能完全存放的下表中所有的記錄,所以在掃描表前邊記錄的時候后邊的記錄可能還在磁盤上,等掃描到后邊記錄的時候可能內存不足,所以需要把前邊的記錄從內存中釋放掉。我們前邊又說過,采用嵌套循環連接算法的兩表連接過程中,被驅動表可是要被訪問好多次的,如果這個被驅動表中的數據特別多而且不能使用索引進行訪問,那就相當於要從磁盤上讀好幾次這個表,這個I/O代價就非常大了,所以我們得想辦法:盡量減少訪問被驅動表的次數。
當被驅動表中的數據非常多時,每次訪問被驅動表,被驅動表的記錄會被加載到內存中,在內存中的每一條記錄只會和驅動表結果集的一條記錄做匹配,之后就會被從內存中清除掉。然后再從驅動表結果集中拿出另一條記錄,再一次把被驅動表的記錄加載到內存中一遍,周而復始,驅動表結果集中有多少條記錄,就得把被驅動表從磁盤上加載到內存中多少次。所以我們可不可以在把被驅動表的記錄加載到內存的時候,一次性和多條驅動表中的記錄做匹配,這樣就可以大大減少重復從磁盤上加載被驅動表的代價了。
join buffer
Mysql中有一個叫做join buffer的概念,join buffer就是執行連接查詢前申請的一塊固定大小的內存,先把若干條驅動表結果集中的記錄裝在這個join buffer中,然后開始掃描被驅動表,每一條被驅動表的記錄一次性和join buffer中的多條驅動表記錄做匹配,因為匹配的過程都是在內存中完成的,所以這樣可以顯著減少被驅動表的I/O代價。
最好的情況是join buffer足夠大,能容納驅動表結果集中的所有記錄,這樣只需要訪問一次被驅動表就可以完成連接操作了。這種加入了join buffer的嵌套循環連接算法稱之為基於塊的嵌套連接(Block Nested-Loop Join)算法。
這個join buffer的大小是可以通過啟動參數或者系統變量join_buffer_size進行配置,默認大小為262144字節(也就是256KB),最小可以設置為128字節。當然,對於優化被驅動表的查詢來說,最好是為被驅動表加上效率高的索引,如果實在不能使用索引,並且自己的機器的內存也比較大可以嘗試調大join_buffer_size的值來對連接查詢進行優化。
另外需要注意的是,驅動表的記錄並不是所有列都會被放到join buffer中,只有查詢列表中的列和過濾條件中的列才會被放到join buffer中,所以再次提醒我們,最好不要把*作為查詢列表,只需要把我們關心的列放到查詢列表就好了,這樣可以在join buffer中放置更多的記錄。
外連接消除
內連接的驅動表和被驅動表的位置可以相互轉換,而左連接和右連接的驅動表和被驅動表是固定的。這就導致內連接可能通過優化表的連接順序來降低整體的查詢成本,而外連接卻無法優化表的連接順序。
外連接和內連接的本質區別就是:對於外連接的驅動表的記錄來說,如果無法在被驅動表中找到匹配ON子句中的過濾條件的記錄,那么該記錄仍然會被加入到結果集中,對應的被驅動表記錄的各個字段使用NULL值填充;而內連接的驅動表的記錄如果無法在被驅動表中找到匹配ON子句中的過濾條件的記錄,那么該記錄會被舍棄
案例:下圖中可以發現驅動表和被驅動表發生了變化,實際上加上了is not null之后被優化成了內連接,就可以利用查詢優化器選擇最優的連接順序了。