SQL 中 left join 的底层原理(各种JOIN的复杂度探究)
01. 前言
寫(xiě)過(guò)或者學(xué)過(guò) SQL 的人應(yīng)該都知道 left join,知道 left join 的實(shí)現(xiàn)的效果,就是保留左表的全部信息,然后把右表往左表上拼接,如果拼不上就是 null。除了 left join 以外,還有 inner join、outer join、right join,這些不同的 join 能達(dá)到的什么樣的效果,大家應(yīng)該都了解了,如果不了解的可以看看網(wǎng)上的帖子或者隨便一本 SQL 書(shū)都有講的。今天我們不講這些 join 能達(dá)到什么效果,我們主要講這些 join 的底層原理是怎么實(shí)現(xiàn)的,也就是具體的效果是怎么呈現(xiàn)出來(lái)的。
join 主要有 Nested Loop、Hash Join、Merge Join 這三種方式,我們這里只講最普遍的,也是最好的理解的 Nested Loop,Nested Loop 翻譯過(guò)來(lái)就是嵌套循環(huán)的意思,那什么又是嵌套循環(huán)呢?嵌套大家應(yīng)該都能理解,就是一層套一層;那循環(huán)呢,你可以理解成是 for 循環(huán)。
Nested Loop 里面又有三種細(xì)分的連接方式,分別是 Simple Nested-Loop Join、Index Nested-Loop Join、Block Nested-Loop Join,接下來(lái)我們就分別去看一下這三種細(xì)分的連接方式。
在正式開(kāi)始之前,先介紹兩個(gè)概念:驅(qū)動(dòng)表(也叫外表)和被驅(qū)動(dòng)表(也叫非驅(qū)動(dòng)表,還可以叫匹配表,亦可叫內(nèi)表),簡(jiǎn)單來(lái)說(shuō),驅(qū)動(dòng)表就是主表,left join 中的左表就是驅(qū)動(dòng)表,right join 中的右表是驅(qū)動(dòng)表。一個(gè)是驅(qū)動(dòng)表,那另一個(gè)就只能是非驅(qū)動(dòng)表了,在 join 的過(guò)程中,其實(shí)就是從驅(qū)動(dòng)表里面依次(注意理解這里面的依次)取出每一個(gè)值,然后去非驅(qū)動(dòng)表里面進(jìn)行匹配,那具體是怎么匹配的呢?這就是我們接下來(lái)講的這三種連接方式。
02.Simple Nested-Loop Join
Simple Nested-Loop Join 是這三種方法里面最簡(jiǎn)單,最好理解,也是最符合大家認(rèn)知的一種連接方式,現(xiàn)在有兩張表 table A 和 table B,我們讓 table A left join table B,如果是用第一種連接方式去實(shí)現(xiàn)的話,會(huì)是怎么去匹配的呢?直接上圖:
上面的 left join 會(huì)從驅(qū)動(dòng)表 table A 中依次取出每一個(gè)值,然后去非驅(qū)動(dòng)表 table B 中從上往下依次匹配,然后把匹配到的值進(jìn)行返回,最后把所有返回值進(jìn)行合并,這樣我們就查找到了 table A left join table B 的結(jié)果。是不是和你的認(rèn)知是一樣的呢?利用這種方法,如果 table A 有10行,table B 有10行,總共需要執(zhí)行 10 x 10 = 100 次查詢。
這種暴力匹配的方式在數(shù)據(jù)庫(kù)中一般不使用。
03.Index Nested-Loop Join
Index Nested-Loop Join 這種方法中,我們看到了 Index,大家應(yīng)該都知道這個(gè)就是索引的意思,這個(gè) Index 是要求非驅(qū)動(dòng)表上要有索引,有了索引以后可以減少匹配次數(shù),匹配次數(shù)減少了就可以提高查詢的效率了。
為什么會(huì)有了索引以后可以減少查詢的次數(shù)呢?這個(gè)其實(shí)就涉及到數(shù)據(jù)結(jié)構(gòu)里面的一些知識(shí)了,給大家舉個(gè)例子就清楚了。
上圖中左邊就是普通列的存儲(chǔ)方式,右邊是樹(shù)結(jié)構(gòu)索引,什么是樹(shù)結(jié)構(gòu)呢?就是數(shù)據(jù)分布的像樹(shù)這樣一層一層的,樹(shù)結(jié)構(gòu)有一個(gè)特點(diǎn)就是左邊的數(shù)據(jù)小于頂點(diǎn)的數(shù),右邊的數(shù)大于頂點(diǎn)的數(shù),你看右圖中,左邊的數(shù)3是不是小于頂點(diǎn)6,右邊的數(shù)7是不是大于頂點(diǎn)6;左邊的數(shù)1是不是小于頂點(diǎn)3,右邊的數(shù)4是不是大于頂點(diǎn)3。
假如我們現(xiàn)在要匹配數(shù)值9,如果是左邊這種數(shù)據(jù)存儲(chǔ)方式的話,我們需要從第一行依次匹配到最后一行才能找到數(shù)值9,總共需要匹配7次;但是如果我們是用右邊這種樹(shù)結(jié)構(gòu)索引的話,我們先拿9和最上層頂點(diǎn)6去匹配,發(fā)現(xiàn)9比6大,我們就去頂點(diǎn)的右邊去找,再去和7匹配,發(fā)現(xiàn)9仍然比7大,再去7的右邊找,就找到了9,這樣我們只匹配了3次就把我們想要的9找到了。是不是相比匹配7次節(jié)省了很多時(shí)間。
數(shù)據(jù)庫(kù)中的索引一般用 B+ 樹(shù),為了讓大家更好的理解,我上面畫(huà)的圖只是最簡(jiǎn)單的一種樹(shù)結(jié)構(gòu),而非真實(shí)的 B+ 樹(shù),但是原理是一樣的。
如果索引是主鍵的話,效率會(huì)更高,因?yàn)橹麈I必須是唯一的,所以如果被驅(qū)動(dòng)表是用主鍵去連接,只會(huì)出現(xiàn)多對(duì)一或者一對(duì)一的情況,而不會(huì)出現(xiàn)多對(duì)多和一對(duì)多的情況。
04.Block Nested-Loop Join
理想情況下,用索引匹配是最高效的一種方式,但是在現(xiàn)實(shí)工作中,并不是所有的列都是索引列,這個(gè)時(shí)候就需要用到 Block Nested-Loop Join 方法了,這種方法與第一種方法比較類似,唯一的區(qū)別就是會(huì)把驅(qū)動(dòng)表中 left join 涉及到的所有列(不止是用來(lái)on的列,還有select部分的列)先取出來(lái)放到一個(gè)緩存區(qū)域,然后再去和非驅(qū)動(dòng)表進(jìn)行匹配,這種方法和第一種方法相比所需要的匹配次數(shù)是一樣的,差別就在于驅(qū)動(dòng)表的列數(shù)不同,也就是數(shù)據(jù)量的多少不同。所以雖然匹配次數(shù)沒(méi)有減少,但是總體的查詢性能還是有提升的。
Join操作是一種常見(jiàn)的數(shù)據(jù)庫(kù)操作,通過(guò)Join可以將多個(gè)表關(guān)聯(lián)起來(lái),根據(jù)用戶的條件共同提供數(shù)據(jù)。一般情況,在數(shù)據(jù)庫(kù)中都會(huì)內(nèi)置多種Join算法,優(yōu)化器在優(yōu)化的時(shí)候會(huì)根據(jù)SQL語(yǔ)句和表的統(tǒng)計(jì)信息選擇合適的算法。
Hash Join
在執(zhí)行Hash Join時(shí),1. 會(huì)根據(jù)Join條件將一張表進(jìn)行Hash運(yùn)算加載到內(nèi)存中的一張Hash表中。Hash表類似與Java中的HashTable;2.遍歷另外一張表,進(jìn)行Hash運(yùn)算后在內(nèi)存中查找滿足條件的記錄。
select * from t1 join t2 on t1.a = t2.b;在執(zhí)行這個(gè)SQL的時(shí)候,先加載表t1的數(shù)據(jù),然后根據(jù)表t1的a字段作為key構(gòu)造Hash表。之后,從表t2中逐條取出記錄,計(jì)算字段b的Hash值,去Hash表中查找是否存在滿足條件的記錄。
Hash Join的性能很高,但是前提條件是內(nèi)存中能夠存放下其中一張表的Hash表。所以一般適用于大小表Join。在一些大數(shù)據(jù)分析的數(shù)據(jù)查詢引擎中,當(dāng)內(nèi)存放不下這種Hash表的時(shí)候,會(huì)將小表進(jìn)行分區(qū)保存到磁盤(pán)上,之后再執(zhí)行Join。
嵌套循環(huán)Join
嵌套循環(huán)Join中,至少一張表存在索引,且Join的條件是對(duì)索引列的比對(duì)。帶有索引的表作為被檢索表,對(duì)不帶有索引或者兩張都帶有索引的表中較小的那張表進(jìn)行遍歷。這個(gè)算法充分利用了索引的優(yōu)勢(shì),讓Join的時(shí)間復(fù)雜度從O(m*n)變成了O(n),其中m為被檢索表的行數(shù),n為遍歷表的行數(shù)。
Merge Hash
相對(duì)于上述兩個(gè)算法,這個(gè)算法的性能差些,但是使用范圍更廣些。在這個(gè)算法中,相對(duì)兩張表中的數(shù)據(jù)進(jìn)行排序,之后再分別取一段進(jìn)行Join。
Semi Join
半連接,對(duì)于左邊的表輸出滿足條件的記錄,而對(duì)于右邊的表則不管是否滿足條件都不會(huì)被輸出,也就是,最終的結(jié)果是左邊表數(shù)據(jù)記錄的一個(gè)子集,類似于in、exists。Semi Join本身就是Join的一種。在大數(shù)據(jù)跨數(shù)據(jù)源的查詢中,Semi Join是對(duì)inner join、left join、right join的一種優(yōu)化。查詢跨數(shù)據(jù)源時(shí),盡量減少?gòu)拿總€(gè)數(shù)據(jù)源出來(lái)的數(shù)據(jù)量是一種很有效的優(yōu)化方式,畢竟網(wǎng)絡(luò)傳輸是要花費(fèi)時(shí)間的。將Join轉(zhuǎn)化成Semi Join是一種有效減小數(shù)據(jù)量的方式。
對(duì)于:select * from t1 join t2 where t1.a = t2.b,Semi Join的過(guò)程如下:
1.將表t1的數(shù)據(jù)加載到內(nèi)存;
2.根據(jù)t1的數(shù)據(jù),改寫(xiě)加載表t2的條件,即將SQL語(yǔ)句改寫(xiě)成in、exists等。假設(shè)表t1中,全部記錄的a字段只有兩個(gè)值:aa和bb,那么SQL將被改寫(xiě)為select * from t2 in (‘a(chǎn)a’,‘bb’);
3.對(duì)從表t1和t2加載的數(shù)據(jù)做Join;
第2步中對(duì)加載t2數(shù)據(jù)的SQL的改寫(xiě),使原本需要加載整個(gè)t2表改為僅加載t2中滿足條件的數(shù)據(jù)。
總結(jié)
以上是生活随笔為你收集整理的SQL 中 left join 的底层原理(各种JOIN的复杂度探究)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 计算机秋招必备!上海互联网大厂企业整理清
- 下一篇: 【一起去大厂系列】深入理解MySQL中w