數(shù)據(jù)庫內(nèi)核雜談 - 表的JOIN(連接)结澄,執(zhí)行復(fù)雜分析語句的基礎(chǔ)

歡迎閱讀數(shù)據(jù)庫內(nèi)核雜談!這期我們重新回歸主線劇情棺榔,繼續(xù)討論執(zhí)行算子的實現(xiàn)彪标。相對簡單的算子如limit或者是projection,在內(nèi)核雜談的第一期(一小時數(shù)據(jù)庫實現(xiàn))就已經(jīng)討論過掷豺,不再贅述捞烟。繼上節(jié)討論了排序和聚合的實現(xiàn)薄声,我們對單個表的大部分操作都覆蓋了。但數(shù)據(jù)庫強大的地方在于题画,它能夠把現(xiàn)實中的某一塊業(yè)務(wù)默辨,映射地表達(dá)成一系列的表的集合,并且其查詢語句SQL支持多個表相關(guān)聯(lián)的查詢操作苍息。這種連接多表的查詢使得數(shù)據(jù)庫的功能得到了一個質(zhì)的飛躍缩幸。對應(yīng)用開發(fā)者而言,相當(dāng)于打開了一個新世界的大門竞思。原本一個個獨立的表表谊,因為連接而產(chǎn)生了無窮多種可能。這期盖喷,咱們就來細(xì)聊一下表與表之間的JOIN(連接)算子的實現(xiàn)爆办。

為什么需要JOIN?

在說具體實現(xiàn)前,咱們先細(xì)聊一下為什么數(shù)據(jù)庫需要支持JOIN课梳。這個問題距辆,甚至可以再退一步到為什么數(shù)據(jù)庫需要有多個表?其實答案很簡單暮刃,上文都提到了:因為現(xiàn)實中的事物就是復(fù)雜跨算,多樣的。多個表之間的關(guān)系能方便地和現(xiàn)實世界的事物映射起來椭懊,并且這種映射更直觀诸蚕,更能夠讓人接受。就像面向?qū)ο缶幊桃粯友踱峙挛覀兌疾荒芟胂笕绻嫦驅(qū)ο缶幊讨荒軇?chuàng)建一種類型的對象吧背犯。

除了方便映射實物,業(yè)務(wù)邏輯同樣需要JOIN狂窑。舉個例子,假設(shè)現(xiàn)在有個簡單的電商系統(tǒng)有買家桑腮,賣家和訂單三張表泉哈,數(shù)據(jù)科學(xué)家想要查詢2018年,對于上浩铺郑客戶的銷售額最高的前3位賣家信息丛晦。這么一個簡單的查詢其實就用到了每個表里的信息,需要從買家表里得到賣家信息提陶,從買家表里得到地址是上海的買家ID烫沙,然后對訂單表以賣家ID做組隊聚合,最后對銷售總額進(jìn)行排序并取前3隙笆。讀者可能想到锌蓄,這些業(yè)務(wù)邏輯其實也可以放在應(yīng)用層做升筏,的確如此(并且,如果是使用某些NoSQL的用戶瘸爽,因為還不支持JOIN您访,應(yīng)用層實現(xiàn)是唯一的選擇)。放在應(yīng)用層有下面這些短處剪决,一是執(zhí)行效率方面肯定不如在數(shù)據(jù)庫高效灵汪;二是數(shù)據(jù)一致性和正確性得不到保證。假設(shè)在多用戶的情形下柑潦,有多個用戶同時在更新數(shù)據(jù)和查詢數(shù)據(jù)享言,如何保證查詢數(shù)據(jù)的正確性。由于應(yīng)用層沒有數(shù)據(jù)庫系統(tǒng)的全局觀渗鬼,在保證對多用戶的支持上實現(xiàn)會更加復(fù)雜览露。業(yè)務(wù)驅(qū)動實現(xiàn),數(shù)據(jù)庫系統(tǒng)就推出了SQL查詢語句讓實現(xiàn)業(yè)務(wù)的程序員通過構(gòu)建不同的SQL語句就能得到相應(yīng)的信息乍钻,而把復(fù)雜的執(zhí)行邏輯肛循,資源管理,多用戶協(xié)調(diào)等等都封裝進(jìn)了數(shù)據(jù)庫內(nèi)部银择。這樣不僅提高了整體執(zhí)行效率多糠,也大大簡化了業(yè)務(wù)邏輯的開發(fā)工作。 這里插個題外話浩考,現(xiàn)在很多互聯(lián)網(wǎng)公司開始推出數(shù)據(jù)中臺夹孔,我覺得和數(shù)據(jù)庫系統(tǒng)提供SQL查詢語句的封裝是類似的概念:由于業(yè)務(wù)更加復(fù)雜,但迭代需求更快析孽,如果每次實現(xiàn)新的業(yè)務(wù)或報表都需要寫很復(fù)雜的查詢語句搭伤,那豈不是效率很低。如何能夠根據(jù)業(yè)務(wù)邏輯需求袜瞬,把數(shù)據(jù)標(biāo)準(zhǔn)化怜俐,接口化,提供比SQL語句更高層次的API來方便上層開發(fā)邓尤。除了更進(jìn)一步提高效率拍鲤,還能提高安全性和對數(shù)據(jù)的控制性。未準(zhǔn)就出現(xiàn)一套新的數(shù)據(jù)操作的標(biāo)準(zhǔn)汞扎,值得期待季稳。

鋪墊做好了,進(jìn)入正題環(huán)節(jié)澈魄。在查詢語句中經(jīng)尘笆螅可能涉及多個表的JOIN,比如我們示例中的訂單痹扇,賣家和買家铛漓。在實現(xiàn)過程中溯香,我們可以選擇兩個表先JOIN變成一個暫存的中間表然后再和下一個表JOIN來獲得最終結(jié)果(當(dāng)然也有一些高級的支持多表同時JOIN的算子操作,就不在這篇進(jìn)行深入了)因此票渠,只要提供兩個表JOIN的算子操作逐哈,就能依次實現(xiàn)多表JOIN,得到最終結(jié)果问顷。今天討論的實現(xiàn)都針對兩個表昂秃。

其次,和討論排序以及聚合一樣杜窄,為了估計和比較時間復(fù)雜度肠骆,我們假設(shè)有兩個表,表A和表B塞耕;分別有M個block(頁)蚀腿,m個row以及N個Block和n個row的數(shù)據(jù),并且假設(shè)M < N 和 m < n扫外。最后莉钙,因為絕大部分的JOIN都涉及equality join,也就是JOIN的where condition里面是等式比如(WHERE A.id = B.id)筛谚,今天討論的JOIN實現(xiàn)都是基于equality join磁玉。

NestedLoopJoin:看似暴力的兩層for循環(huán)

如果有兩個表,讓你在應(yīng)用層做join驾讲,最容易想到的是什么方法蚊伞?你可能應(yīng)口而出,可以用兩層for循環(huán):第一層for循環(huán)表A的每一個row吮铭,然后嵌套內(nèi)部另一層表B的for循環(huán)时迫,循環(huán)體內(nèi)做具體的join邏輯,如果滿足條件谓晌,就輸出一個joint的結(jié)果掠拳,代碼示例如下圖所示:

NestedLoopJoin

這就是我們今天介紹的第一個JOIN算子實現(xiàn),NestedLoopJoin纸肉;名副其實的兩層for循環(huán)溺欧。如果表A和表B都能放進(jìn)內(nèi)存里,那總共的IO時間復(fù)雜度就是M + N:只需要讀取M + N個page毁靶。那如果內(nèi)存有限胧奔,每次每個表只能讀取一個page逊移,又該怎么改進(jìn)呢预吆?改進(jìn)的代碼如下圖所示:

block-based NestedLoopJoin

如代碼所示,先讀取一個外層表的block胳泉,然后依次讀取內(nèi)層表的block拐叉,比較結(jié)束后再讀取下一個岩遗。這種時間下的IO cost是多少?應(yīng)該是 M + M * N凤瘦。由此也可見宿礁,在運行NestedLoopJoin時,應(yīng)該盡量把小的表放在外層循環(huán)中來減少IO次數(shù)蔬芥。

這里梆靖,給大家出一道思考題,現(xiàn)在假設(shè)系統(tǒng)允許分配B個page給這個join算子笔诵,應(yīng)該怎么分配讀取才能最優(yōu)返吻,最后IO時間復(fù)雜度又是多少呢?

有同學(xué)問乎婿,還能不能進(jìn)一步優(yōu)化测僵,答案是可以的。時間復(fù)雜度的大頭主要在M * N谢翎, 有什么辦法可以優(yōu)化這一塊捍靠?要想辦法避免對表B進(jìn)行全表掃描。優(yōu)化方法就是森逮,如果表B在對應(yīng)的join鍵上建立索引榨婆,那我們就能用Index Scan來取代全表掃描。這就是NestedLoopIndexJoin吊宋。假設(shè)每次Index Scan的時間是常數(shù)C纲辽,它的時間復(fù)雜度就是 M + m * C。相較于前面 block-based NestedLoopJoin又提高了不少璃搜。

總結(jié)一下拖吼,我們第一個介紹的Join實現(xiàn)就是NestedLoopJoin,看似暴力的2層for循環(huán)这吻。同時吊档,也引出了block-based和內(nèi)層循環(huán)用IndexScan替代的優(yōu)化。

SortMergeJoin:Merge 2 sorted List

做過LeetCode的同學(xué)肯定都知道這題唾糯,Merge 2 sorted list怠硼。只要對每個list維護(hù)一個指針,然后相互比較后移即可移怯。其實這個思路同樣可以應(yīng)用到表的JOIN上香璃,只要對兩個表分別根據(jù)JOIN鍵做排序,然后在JOIN時舟误,對每個表維護(hù)一個指針葡秒,當(dāng)兩邊指針的鍵值相同,就可以輸出JOIN的row。指針不斷后移眯牧,直到一方終止為止蹋岩。代碼如下圖所示:

SortMergeJoin

從IO的時間復(fù)雜度來看,又是多少呢学少?在講排序那章的時候我們已經(jīng)討論過剪个,對M個page的表做排序,復(fù)雜度是2M * (log2M)版确。因此扣囊,總共的時間復(fù)雜度是M + N + 2M * (log2M) + 2N * (log2N)。因為沒有M*N這一項绒疗,因此當(dāng)兩個表都比較大的情況下如暖,性能是優(yōu)于NestedLoopJoin的。

除了性能進(jìn)步忌堂,SortMergeJoin還有什么好的特性盒至?和BTreeIndexScan一樣,SortMergeJoin后輸出的tuples是已經(jīng)排過序的士修。因此枷遂,如果上層的SQL語句還有對應(yīng)鍵排序的需求,就不再需要額外排序了棋嘲。并且排序的需求不單單指ORDER BY語句酒唉,上一期提到過的,作組隊聚合(group aggregation)的時候沸移,有序的tuple就不需要額外建立Hash表痪伦,可以直接用SortGroupByAgg來實現(xiàn)。另外雹锣,如果系統(tǒng)在JOIN前發(fā)現(xiàn)一方或者兩方都已經(jīng)排序過网沾,同樣在JOIN時就可以直接使用MERGE JOIN。很多情況下蕊爵,排序是一個一勞永逸的過程辉哥,一旦排序后,上層的語句可能都能獲益攒射。

總結(jié)一下醋旦,本章介紹的第二個JOIN算子實現(xiàn)是SortMergeJoin,分別對兩張表根據(jù)JOIN鍵排序然后合并会放。

HashJoin:最高效的Join

說完了利用排序來作JOIN饲齐,結(jié)合上一節(jié)篇章中我們提過的,實現(xiàn)聚類可以用排序或者哈希表咧最。排序和哈希表在數(shù)據(jù)庫算子里有那么些既生瑜何生亮的即視感捂人。你是不是也預(yù)料到了甩骏,在表JOIN的時候,哈希表依然作為排序的對手出現(xiàn)先慷。這正是我們要介紹的最后一種實現(xiàn)HashJoin,并且HashJoin再一次扮演了亮的角色咨察。HashJoin算法很直觀论熙,對相對較小的表表A,根據(jù)join condition來建立哈希表摄狱。然后對表B脓诡,只要依次讀入數(shù)據(jù)去和哈希表里的值去匹配,如果匹配媒役,那就生成一個新的tuple祝谚。代碼如下圖所示:

HashJoin

下面來計算IO時間復(fù)雜度,如果假設(shè)較小的表A建立的哈希表能夠全部放進(jìn)內(nèi)存的話酣衷,IO時間復(fù)雜度只有 M+N交惯。

按照這種方式,假設(shè)將較小的外部表M建立哈希表穿仪,如果能全部放進(jìn)內(nèi)存的話席爽,然后對N表進(jìn)行全表掃描并且對比hash。IO時間復(fù)雜度只有 M + N啊片。如果表A比較大只锻,不能全部放進(jìn)內(nèi)存的話,我們又要借助外部哈希表的算法來分而治之紫谷。首先齐饮,我們用一個哈希函數(shù)把表A和表B分別劃分到x個bucket,如果在劃分的過程中發(fā)現(xiàn)某個bucket依然很大笤昨,可以借助遞歸思想祖驱,繼續(xù)劃分。我們稱之為partition phase瞒窒。Partition phase的IO復(fù)雜度是2 * (M + N)羹膳,因為需要把所有的表A和表B讀取到內(nèi)存在寫回文件系統(tǒng)。第二階段就是對于每個bucket根竿,分別讀取表A和表B然后建立哈希表做JOIN陵像,稱之為probing phase。這個階段會再次讀取所有表A和表B的數(shù)據(jù)寇壳,因此是 M + N醒颖。因此,HASH JOIN的時間復(fù)雜度壳炎,在即使需要依賴外部哈希表的情況下泞歉,依然是線性的 3*(M+N)逼侦。所以在絕大多數(shù)的情況下,性能都是最優(yōu)的腰耙。

總結(jié)一下榛丢,本章介紹的最后一個JOIN實現(xiàn)是HashJoin,通過對相對較小的表建立哈希表挺庞,然后讀取另一個表來匹配生成join rows晰赞。

總結(jié)

總結(jié)時刻,本章我們詳細(xì)介紹了三種JOIN算子的實現(xiàn)选侨,分析了它們的適用場景和時間復(fù)雜度掖鱼。至此,數(shù)據(jù)庫內(nèi)和雜談覆蓋了常見算子的實現(xiàn)援制。各位讀者可以回想一下寫過的SQL戏挡,是否能能夠使用我們討論過的算子組合成算子樹來實現(xiàn)這些查詢語句。

說了那么多種JOIN晨仑,讀者可能會想褐墅,即然已經(jīng)說了HashJoin普遍情況下性能最優(yōu),為什么還需要實現(xiàn)其他JOIN呢洪己?因為在特定的情況下掌栅,NestedLoopIndexScan可能會比HashJoin更快,或者在需要排序的情況下码泛,SortMergeJoin也可能更有優(yōu)勢猾封。影響因素有具體的查詢語句,表A和表B的大小噪珊,join鍵值的分布晌缘,以及是否對join鍵值有index等等。數(shù)據(jù)庫在執(zhí)行語句的時候痢站,需要通盤考慮這些影響因素來決定最后具體使用哪種JOIN算子磷箕。而做這個決定的就是咱們下一期要講的內(nèi)容:數(shù)據(jù)庫的大腦 -- 優(yōu)化器。盡情期待阵难。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末岳枷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子呜叫,更是在濱河造成了極大的恐慌空繁,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件朱庆,死亡現(xiàn)場離奇詭異盛泡,居然都是意外死亡,警方通過查閱死者的電腦和手機娱颊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門傲诵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凯砍,“玉大人,你說我怎么就攤上這事拴竹∥蝰茫” “怎么了?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵栓拜,是天一觀的道長座泳。 經(jīng)常有香客問我,道長菱属,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任舰罚,我火速辦了婚禮纽门,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘营罢。我一直安慰自己赏陵,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布饲漾。 她就那樣靜靜地躺著蝙搔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪考传。 梳的紋絲不亂的頭發(fā)上吃型,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天,我揣著相機與錄音僚楞,去河邊找鬼勤晚。 笑死,一個胖子當(dāng)著我的面吹牛泉褐,可吹牛的內(nèi)容都是我干的赐写。 我是一名探鬼主播,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼膜赃,長吁一口氣:“原來是場噩夢啊……” “哼挺邀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起跳座,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤端铛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后疲眷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沦补,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年咪橙,在試婚紗的時候發(fā)現(xiàn)自己被綠了夕膀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虚倒。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡隧期,死狀恐怖泰讽,靈堂內(nèi)的尸體忽然破棺而出涣脚,到底是詐尸還是另有隱情舀患,我是刑警寧澤水评,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布著隆,位于F島的核電站昼伴,受9級特大地震影響茶袒,放射性物質(zhì)發(fā)生泄漏准颓。R本人自食惡果不足惜哈蝇,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望攘已。 院中可真熱鬧炮赦,春花似錦、人聲如沸样勃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽峡眶。三九已至剧防,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間辫樱,已是汗流浹背峭拘。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留狮暑,地道東北人棚唆。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像心例,于是被迫代替她去往敵國和親宵凌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容