《DBNotes:Join算法的前世今生》
目錄
- NestLoopJoin算法
- Simple Nested-Loop Join
- Index Nested-Loop Join
- Block Nested-Loop Join
- Batched Key Access
- Hash Join算法
- In-Memory Join(CHJ)
- On-Disk Hash Join
- 參考鏈接
在8.0.18之前,MySQL只支持NestLoopJoin算法,最簡(jiǎn)單的就是Simple NestLoop Join,MySQL針對(duì)這個(gè)算法做了若干優(yōu)化,實(shí)現(xiàn)了Block NestLoop Join,Index NestLoop Join和Batched Key Access等,有了這些優(yōu)化,在一定程度上能緩解對(duì)HashJoin的迫切程度。但是HashJoin的支持使得MySQL優(yōu)化器有更多選擇,SQL的執(zhí)行路徑也能做到更優(yōu),尤其是對(duì)于等值join的場(chǎng)景。
NestLoopJoin算法
長(zhǎng)期以來(lái),在MySQL中執(zhí)行聯(lián)接的唯一算法是嵌套循環(huán)算法的變體。
Simple Nested-Loop Join
如果我們執(zhí)行這樣一條等值查詢語(yǔ)句:
select * from t1 straight_join t2 on (t1.a=t2.b);由于表 t2 的字段 b 上沒(méi)有索引,每次到 t2 去匹配的時(shí)候,就要做一次全表掃描。就相當(dāng)于是雙for循環(huán)。如果 t1 和 t2 都是 10 萬(wàn)行的表(當(dāng)然了,這也還是屬于小表的范圍),就要掃描 100 億行。
SimpleNestLoopJoin顯然是很低效的,對(duì)內(nèi)表需要進(jìn)行N次全表掃描,實(shí)際復(fù)雜度是N*M,N是外表的記錄數(shù)目,M是記錄數(shù),代表一次掃描內(nèi)表的代價(jià)。為此,MySQL針對(duì)SimpleNestLoopJoin做了若干優(yōu)化。
Index Nested-Loop Join
如果我們能對(duì)內(nèi)表的join條件建立索引,那么對(duì)于外表的每條記錄,無(wú)需再進(jìn)行全表掃描內(nèi)表,只需要一次Btree-Lookup即可,整體時(shí)間復(fù)雜度降低為N*O(logM)。
再來(lái)看看這一句
在這條語(yǔ)句里,被驅(qū)動(dòng)表 t2 的字段 a 上有索引,join 過(guò)程用上了這個(gè)索引,因此這個(gè)語(yǔ)句的執(zhí)行流程是這樣的:
執(zhí)行流程示意圖如下:
對(duì)比HashJoin,對(duì)于外表每條記錄,HashJoin是一次HashTable的search,當(dāng)然HashTable也有build時(shí)間,還需要處理內(nèi)存不足的情況,不一定比INLJ好。
Block Nested-Loop Join
MySQL采用了批量技術(shù),即一次利用join_buffer_size緩存足夠多的記錄,每次遍歷內(nèi)表時(shí),每條內(nèi)表記錄與這一批數(shù)據(jù)進(jìn)行條件判斷,這樣就減少了掃描內(nèi)表的次數(shù),如果內(nèi)表比較大,間接就緩解了IO的讀壓力。
Simple Nested-Loop Join 與 Block Nested-Loop Join從時(shí)間復(fù)雜度上來(lái)說(shuō),這兩個(gè)算法是一樣的。但是,Block Nested-Loop Join是內(nèi)存操作,速度上會(huì)快很多,性能也更好。
示意圖如下:
Batched Key Access
IndexNestLoopJoin利用join條件的索引,通過(guò)Btree-Lookup去匹配減少了遍歷內(nèi)表的代價(jià)。如果join條件是非主鍵列,那么意味著大量的回表和隨機(jī)IO。BKA優(yōu)化的做法是,將滿足條件的一批數(shù)據(jù)按主鍵排序,這樣回表時(shí),從主鍵的角度來(lái)說(shuō)就相對(duì)有序,緩解隨機(jī)IO的代價(jià)。BKA實(shí)際上是利用了MRR特性(MultiRangeRead),訪問(wèn)數(shù)據(jù)之前,先將主鍵排序,然后再訪問(wèn)。主鍵排序的緩存大小通過(guò)參數(shù)read_rnd_buffer_size控制。
Hash Join算法
NestLoopJoin算法簡(jiǎn)單來(lái)說(shuō),就是雙重循環(huán),遍歷外表(驅(qū)動(dòng)表),對(duì)于外表的每一行記錄,然后遍歷內(nèi)表,然后判斷join條件是否符合,進(jìn)而確定是否將記錄吐出給上一個(gè)執(zhí)行節(jié)點(diǎn)。從算法角度來(lái)說(shuō),這是一個(gè)M*N的復(fù)雜度。HashJoin是針對(duì)equal-join場(chǎng)景的優(yōu)化,基本思想是,將外表數(shù)據(jù)load到內(nèi)存,并建立hash表,這樣只需要遍歷一遍內(nèi)表,就可以完成join操作,輸出匹配的記錄。如果數(shù)據(jù)能全部load到內(nèi)存當(dāng)然好,邏輯也簡(jiǎn)單,一般稱這種join為CHJ(Classic Hash Join),之前MariaDB就已經(jīng)實(shí)現(xiàn)了這種HashJoin算法。如果數(shù)據(jù)不能全部load到內(nèi)存,就需要分批load進(jìn)內(nèi)存,然后分批join,下面具體介紹這幾種join算法的實(shí)現(xiàn)。
In-Memory Join(CHJ)
HashJoin一般包括兩個(gè)過(guò)程,創(chuàng)建hash表的build過(guò)程和探測(cè)hash表的probe過(guò)程。
1).build phase
遍歷外表,以join條件為key,查詢需要的列作為value創(chuàng)建hash表。這里涉及到一個(gè)選擇外表的依據(jù),主要是評(píng)估參與join的兩個(gè)表(結(jié)果集)的大小來(lái)判斷,誰(shuí)小就選擇誰(shuí),這樣有限的內(nèi)存更容易放下hash表。
2).probe phase
hash表build完成后,然后逐行遍歷內(nèi)表,對(duì)于內(nèi)表的每個(gè)記錄,對(duì)join條件計(jì)算hash值,并在hash表中查找,如果匹配,則輸出,否則跳過(guò)。所有內(nèi)表記錄遍歷完,則整個(gè)過(guò)程就結(jié)束了
On-Disk Hash Join
CHJ的限制條件在于,要求內(nèi)存能裝下整個(gè)外表。在MySQL中,Join可以使用的內(nèi)存通過(guò)參數(shù)join_buffer_size控制。如果join需要的內(nèi)存超出了join_buffer_size,那么CHJ將無(wú)能為力,只能對(duì)外表分成若干段,每個(gè)分段逐一進(jìn)行build過(guò)程,然后遍歷內(nèi)表對(duì)每個(gè)分段再進(jìn)行一次probe過(guò)程。假設(shè)外表分成了N片,那么將掃描內(nèi)表N次。這種方式當(dāng)然是比較弱的。
在MySQL8.0中,如果join需要內(nèi)存超過(guò)了join_buffer_size,build階段會(huì)首先利用hash算將外表進(jìn)行分區(qū),并產(chǎn)生臨時(shí)分片寫到磁盤上;然后在probe階段,對(duì)于內(nèi)表使用同樣的hash算法進(jìn)行分區(qū)。由于使用分片hash函數(shù)相同,那么key相同(join條件相同)必然在同一個(gè)分片編號(hào)中。接下來(lái),再對(duì)外表和內(nèi)表中相同分片編號(hào)的數(shù)據(jù)進(jìn)行CHJ的過(guò)程,所有分片的CHJ做完,整個(gè)join過(guò)程就結(jié)束了。這種算法的代價(jià)是,對(duì)外表和內(nèi)表分別進(jìn)行了兩次讀IO,一次寫IO。相對(duì)于之之前需要N次掃描內(nèi)表IO,現(xiàn)在的處理方式更好。
順序?yàn)?#xff1a;外表的分片、內(nèi)表分片、哈希連接
參考鏈接
join語(yǔ)句怎么優(yōu)化?
MySQL8.0 新特性 Hash Join
哈希加入MySQL 8
MySQL · 新特征 · MySQL 哈希連接實(shí)現(xiàn)介紹
總結(jié)
以上是生活随笔為你收集整理的《DBNotes:Join算法的前世今生》的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 《源码分析转载收藏向—数据库内核月报》
- 下一篇: 《搜索算法——DFS、BFS、回溯》