一、數(shù)據(jù)庫的 join 查詢
數(shù)據(jù)庫提供了多種類型的連接方式,它們之間的區(qū)別在于:從相互交疊的不同數(shù)據(jù)集合中選擇用于連接的行時所采用的方法不同。
A.內(nèi)連接
內(nèi)連接,即最常見的等值連接。【兩邊的表都加限制】
B.外連接
- 左外連接:左表不加限制,保留左表的數(shù)據(jù),匹配右表,右表沒有匹配到的行中的列顯示為 null。【左外連接就是在等值連接的基礎上加上左表中的未匹配數(shù)據(jù)】
- 右外連接:右表不加限制,保留右表的數(shù)據(jù),匹配左表,左表沒有匹配到的行中列顯示為 null。【右外連接就是在等值連接的基礎上加上右表中的未匹配數(shù)據(jù)】
- 完全外連接:左右表都不加限制。即結果為:左右表匹配的數(shù)據(jù)+左表沒有匹配到的數(shù)據(jù)+右表沒有匹配到的數(shù)據(jù)。【完全外連接就是在等值連接的基礎上將左表和右表的未匹配數(shù)據(jù)都加上】
連接的語法:【通常外連接省略outer關鍵字】
left/right/full outer join …on
left/right/full join …on
(+)號的作用:+號可以理解為補充的意思,即哪個表有加號,這個表就是匹配表。加在右表的列上代表右表為補充,為左外連接。加在左表的列上代表左表為補充,為右外連接。
注意:完全外連接不支持(+)寫法。
創(chuàng)建兩種表,生出測試數(shù)據(jù):
CREATE TABLE TQA (
id number,
name VARCHAR2(10)
);
CREATE TABLE TUB (
id number,
name VARCHAR2(10)
);
INSERT INTO TQA VALUES(1,‘QA’);
INSERT INTO TQA VALUES(2,‘QB’);
INSERT INTO TQA VALUES(3,‘QC’);
INSERT INTO TQA VALUES(4,‘QD’);
INSERT INTO TQA VALUES(5,‘QE’);
INSERT INTO TUB VALUES(1,‘UA’);
INSERT INTO TUB VALUES(1,‘UB’);
INSERT INTO TUB VALUES(2,‘UC’);
INSERT INTO TUB VALUES(1,‘UD’);
INSERT INTO TUB VALUES(7,‘UE’);
1??左外連接
select * from TQA a left join TUB b on a.id=b.id;
select * from TQA a,TUB b where a.id=b.id(+);
2??右外連接
select * from TQA a right join TUB b on a.id = b.id;
select * from TQA a,TUB b where a.id(+)=b.id;
3??完全外連接
select * from TQA a full join TUB b on a.id=b.id;
4??等值連接(內(nèi)連接也可省略關鍵字inner,直接寫成join)
select * from TQA a,TUB b where a.id=b.id;
select * from TQA a join TUB b on a.id=b.id;~~~~~等值連接也可以這樣寫
select a.name,b.age from TQA a join TUB b using(id)【`using(id)`等價于`on a.id=b.id`】
注意:等值連接和完全外連接是有區(qū)別的。等值連接是只把滿足條件的兩個表的行相連,然后顯示出來。完全外連接是把匹配查詢條件的、左表沒有匹配到的、右表沒有匹配到的行都顯示出來。
二、總結
SQL 連接(inner/outer join)包括以下:
- 內(nèi)連接(兩邊的表都加限制)–[inner] join
- 左外連接(左邊的表不加限制)–left [outer] join
- 右外連接(右邊的表不加限制)–right [outer] join
- 全外連接(左右兩表都不加限制)–full [outer] join
在左外連接和右外連接時都會以主表為基礎表,該表的內(nèi)容會全部顯示,然后加上主表和匹配表匹配的內(nèi)容。 如果主表的數(shù)據(jù)在匹配表中沒有記錄,那么在相關聯(lián)的結果集行中列顯示為空值(null)。
內(nèi)連接,可以使用"(+)",但是必須省略。即兩張表均為"主表",都不是匹配表。而對于外連接, 也可以使用“(+) ”來表示。關于外連接使用(+)的一些注意事項:
- (+)操作符只能出現(xiàn)在 where 子句中,并且不能與 outer join 語法同時使用。
- 當使用(+)操作符執(zhí)行外連接時,如果在 where 子句中包含有多個條件,則必須在所有條件中都包含(+)操作符。
- (+)操作符只適用于列,而不能用在表達式上。
- (+)操作符不能與 or 和 in 操作符一起使用。
- (+)操作符只能用于實現(xiàn)左外連接和右外連接,而不能用于實現(xiàn)完全外連接。
三、注意
left jon on:當 on 條件存在多個時候(left join on ... and ...)會出現(xiàn)一些與預期不符的查詢結果。left join on 多條件失效,會導致主表的記錄全部查出來,and 條件沒有起作用。回顧 left join 的定義,主表會返回所有行,所以 left join 如果對左邊表進行約束的話是不會生效的;但是,對 left join 的右邊表添加條件的話是生效的!反之,right join 同理。
create table a(f1 int, f2 int, index(f1))engine=innodb;
create table b(f1 int, f2 int)engine=innodb;
insert into a values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6);
insert into b values(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);
表 a 和 b 都有兩個字段 f1 和 f2,不同的是表 a 的字段 f1 上有索引。然后,兩個表中都插了 6 條記錄,其中在表 a 和 b 中同時存在的數(shù)據(jù)有 4 行。
1??兩個表 join 包含多個條件的等值匹配,是都要寫到 on 里面,還是只把一個條件寫到 on 里面,其他條件寫到 where 部分?也就是如下 Q1 和 Q2 有何區(qū)別?
select * from a left join b on(a.f1=b.f1) and (a.f2=b.f2);/*Q1*/
select * from a left join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q2*/
這兩個語句的語義邏輯并不相同。二者執(zhí)行結果如下:
可以看到:
- Q1 返回的數(shù)據(jù)集是 6 行,表 a 中即使沒有滿足匹配條件的記錄,查詢結果中也會返回一行,并將表 b 的各個字段值填成 NULL。
- Q2 返回的數(shù)據(jù)集是 4 行。從邏輯上可以這么理解,最后兩行,由于表 b 中沒有匹配的字段,結果集里面 b.f2 的值是空,不滿足 where 的條件判斷,因此不能作為結果集的一部分。
Q1 的 explain 結果:
Q1 結果符合預期:驅(qū)動表是表 a,被驅(qū)動表是表 b。由于表 b 的 f1 字段上沒有索引,所以使用的是 Block Nexted Loop Join(簡稱BNL) 算法。由此,這條語句的執(zhí)行流程如下:
- 把表 a 的內(nèi)容讀入 join_buffer 中。因為是 select *,所以字段 f1 和 f2 都被放入 join_buffer 了。
- 順序掃描表 b,對于每一行數(shù)據(jù),判斷 join 條件(也就是a.f1=b.f1 and a.f2=b.f2)是否滿足,滿足條件的記錄,作為結果集的一行返回。如果語句中有 where 子句,需要先判斷 where 部分滿足條件后,再返回。
- 表 b 掃描完成后,對于沒有被匹配的表 a 的行(也就是(1,1)、(2,2)這兩行),把剩余字段補上 NULL,再放入結果集中。
可以看到,這條語句確實是以表 a 為驅(qū)動表,而且從執(zhí)行效果看,也和使用 straight_join 是一樣的。
2??如果用 left join 的話,左邊的表一定是驅(qū)動表嗎?
語句 Q2 的查詢結果里面少了最后兩行數(shù)據(jù),是不是就是把上面流程中的步驟 3 去掉呢?看下語句 Q2 的 explain 結果:
可以看到,這條語句是以表 b 為驅(qū)動表的。而如果一條 join 語句的 Extra 字段什么都沒寫的話,就表示使用的是 Index Nested-Loop Join(簡稱NLJ)算法。因此,Q2 的執(zhí)行流程:順序掃描表 b,每一行用 b.f1 到表 a 中去查,匹配到記錄后判斷 a.f2=b.f2 是否滿足,滿足條件的話就作為結果集的一部分返回。
Q1 和 Q2 這兩個執(zhí)行流程為什么會有這么大差距?其實,這是因為優(yōu)化器基于 Q2 這個查詢的語義做了優(yōu)化。
Q2 里面 where a.f2=b.f2 就表示,查詢結果里面不會包含 b.f2 是 NULL 的行,如此 Q2 的語義就是“找到這兩個表里面,f1、f2 對應相同的行。對于表 a 中存在,而表 b 中匹配不到的行,就放棄”。所以 Q2 雖然用的是 left join,但是語義跟 join 是一致的。
因此,優(yōu)化器就把這條語句的 left join 改寫成了 join,然后因為表 a 的 f1 上有索引,就把表 b 作為驅(qū)動表,這樣就可以用上 NLJ 算法。在執(zhí)行 explain 之后,再執(zhí)行 show warnings,就能看到這個改寫的結果,如圖:這個例子說明,即使在 SQL 語句中寫成 left join,執(zhí)行過程還是有可能不是從左到右連接的。也就是說,使用 left join 時,左邊的表不一定是驅(qū)動表。
3??這樣看來,如果需要 left join 的語義,就不能把被驅(qū)動表的字段放在 where 條件里面做等值判斷或不等值判斷,必須都寫在 on 里面。那如果是 join 語句呢?
select * from a join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q3*/
select * from a join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q4*/
執(zhí)行 explain 和 show warnings,看看優(yōu)化器是怎么做的:可以看到,這兩條語句都被改寫成:
select * from a join b where (a.f1=b.f1) and (a.f2=b.f2);
執(zhí)行計劃自然也是一模一樣的。也就是說,在這種情況下,join 將判斷條件是否全部放在 on 部分就沒有區(qū)別了。
四、Simple Nested Loop Join 的性能問題
join 語句使用不同的算法,對語句的性能影響會很大。雖然 BNL 算法和 Simple Nested Loop Join 算法都是要判斷 M*N 次(M和N分別是join的兩個表的行數(shù)),但是 Simple Nested Loop Join 算法的每輪判斷都要走全表掃描,因此性能上 BNL 算法執(zhí)行起來會快很多。
1??BNL 算法的執(zhí)行邏輯
- 首先,將驅(qū)動表的數(shù)據(jù)全部讀入內(nèi)存 join_buffer 中,這里 join_buffer 是無序數(shù)組。
- 然后,順序遍歷被驅(qū)動表的所有行,每一行數(shù)據(jù)都跟 join_buffer 中的數(shù)據(jù)進行匹配,匹配成功則作為結果集的一部分返回。
2??Simple Nested Loop Join算法的執(zhí)行邏輯
順序取出驅(qū)動表中的每一行數(shù)據(jù),到被驅(qū)動表去做全表掃描匹配,匹配成功則作為結果集的一部分返回。
Simple Nested Loop Join 算法,其實也是把數(shù)據(jù)讀到內(nèi)存里,然后按照匹配條件進行判斷,為什么性能遠不如 BNL 算法?
解釋這個問題,需要用到 MySQL 中索引結構和 Buffer Pool 的相關知識點:
在對被驅(qū)動表做全表掃描的時候,如果數(shù)據(jù)沒有在 Buffer Pool 中,就需要等待這部分數(shù)據(jù)從磁盤讀入;從磁盤讀入數(shù)據(jù)到內(nèi)存中,會影響正常業(yè)務的 Buffer Pool 命中率,而且這個算法天然會對被驅(qū)動表的數(shù)據(jù)做多次訪問,更容易將這些數(shù)據(jù)頁放到 Buffer Pool 的頭部。
即使被驅(qū)動表數(shù)據(jù)都在內(nèi)存中,每次查找“下一個記錄的操作”,都是類似指針操作。而 join_buffer 中是數(shù)組,遍歷的成本更低。