深入理解Oracle表(5):三大表連線方式詳解之Hash Join的定義,原理,演算法,成本,模式和點陣圖
Hash Join的執行計劃第1個是hash表(build table),第2個探查表(probe table),一般不叫內外表,nested loop才有內外表
Hash表也就是所謂的內表,探查表所謂的外表
兩者的執行計劃形如:
nested loop
outer table --驅動表
inner table
hash join
build table (inner table) --驅動表
probe table (outer table)
先看一張圖片,大致瞭解Hash Join的過程:
下面詳細瞭解一下Hash Join
㈠ Hash join概念
Hash join演算法的一個基本思想就是根據小的row sources(稱作build input 也就是前文提到的build table,我們記較小的表為S,較大的表為B)
建立一個可以存在於hash area記憶體中的hash table
然後用大的row sources(稱作probe input,也就是前文提到的probe table) 來探測前面所建的hash table
如果hash area記憶體不夠大,hash table就無法完全存放在hash area記憶體中
針對這種情況,Oracle在連線鍵利用一個hash函式將build input和probe input分割成多個不相連的分割槽
分別記作Si和Bi,這個階段叫做分割槽階段;然後各自相應的分割槽,即Si和Bi再做Hash join,這個階段叫做join階段
如果HASH表太大,無法一次構造在記憶體中,則分成若干個partition,寫入磁碟的temporary segment,則會多一個寫的代價,會降低效率
至於小表的概念,對於 hash join 來說,能容納在 pga 中的 hash table 都可以叫小表,通常比如:
pga_aggregate_target big integer 1073741824
hash area size 大體能使用到40多 M ,這樣的話通常可能容納 幾十萬的記錄
hash area size預設是2*sort_area_size,我們可以直接修改SORT_AREA_SIZE 的大小,HASH_AREA_SIZE也會跟著改變的
如果你的workarea_size_policy=auto,那麼我們只需設定pga_aggregate_target
但請記住,這是一個session級別的引數,有時,我們更傾向於把hash_area_size的大小設成驅動表的1.6倍左右
驅動表僅僅用於nested loop join 和 hash join,但Hash join不需要在驅動表上存在索引,而nested loop join則迫切需求
一兩百萬記錄的表 join上 千萬記錄的表,hash join的通常表現非常好
不過,多與少,大與小,很多時候很難量化,具體情況還得具體分析
如果在分割槽後,針對某個分割槽所建的hash table還是太大的話,oracle就採用nested loop hash join
所謂的nested-loops hash join就是對部分Si建立hash table,然後讀取所有的Bi與所建的hash table做連線
然後再對剩餘的Si建立hash table,再將所有的Bi與所建的hash table做連線,直至所有的Si都連線完了
㈡ Hash Join原理
考慮以下兩個資料集:
S={1,1,1,3,3,4,4,4,4,5,8,8,8,8,10}
B={0,0,1,1,1,1,2,2,2,2,2,2,3,8,9,9,9,10,10,11}
Hash Join的第一步就是判定小表(即build input)是否能完全存放在hash area記憶體中
如果能完全存放在記憶體中,則在記憶體中建立hash table,這是最簡單的hash join
如果不能全部存放在記憶體中,則build input必須分割槽。分割槽的個數叫做fan-out
Fan-out是由hash_area_size和cluster size來決定的。其中cluster size等於db_block_size * _hash_multiblock_io_count
hash_multiblock_io_count是個隱藏引數,在9.0.1以後就不再使用了
[sql] view plaincopyprint?
- [email protected]> ed
- Wrote file afiedt.buf
- 1 select a.ksppinm name,b.ksppstvl value,a.ksppdesc description
- 2 from x$ksppi a,x$ksppcv b
- 3 where a.indx = b.indx
- 4* and a.ksppinm like'%hash_multiblock_io_count%'
- [email protected]> /
- NAME VALUE DESCRIPTION
- ------------------------------ ----- ------------------------------------------------------------
- _hash_multiblock_io_count 0 number of blocks hash join will read/write at once
Oracle採用內部一個hash函式作用於連線鍵上,將S和B分割成多個分割槽
在這裡我們假設這個hash函式為求餘函式,即Mod(join_column_value,10)
這樣產生十個分割槽,如下表:
經過這樣的分割槽之後,只需要相應的分割槽之間做join即可(也就是所謂的partition pairs)
如果有一個分割槽為NULL的話,則相應的分割槽join即可忽略
在將S表讀入記憶體分割槽時,oracle即記錄連線鍵的唯一值,構建成所謂的點陣圖向量
它需要佔hash area記憶體的5%左右。在這裡即為{1,3,4,5,8,10}
當對B表進行分割槽時,將每一個連線鍵上的值與點陣圖向量相比較,如果不在其中,則將其記錄丟棄
在我們這個例子中,B表中以下資料將被丟棄{0,0,2,2,2,2,2,2,9,9,9,9,9}
這個過程就是點陣圖向量過濾
當S1,B1做完連線後,接著對Si,Bi進行連線
這裡oracle將比較兩個分割槽,選取小的那個做build input,就是動態角色互換
這個動態角色互換髮生在除第一對分割槽以外的分割槽上面
㈢ Hash Join演算法
第1步:判定小表是否能夠全部存放在hash area記憶體中,如果可以,則做記憶體hash join。如果不行,轉第二步
第2步:決定fan-out數
(Number of Partitions) * C<= Favm *M
其中C為Cluster size,其值為DB_BLOCK_SIZE*HASH_MULTIBLOCK_IO_COUNT
Favm為hash area記憶體可以使用的百分比,一般為0.8左右
M為Hash_area_size的大小
第3步:讀取部分小表S,採用內部hash函式(這裡稱為hash_fun_1)
將連線鍵值對映至某個分割槽,同時採用hash_fun_2函式對連線鍵值產生另外一個hash值
這個hash值用於建立hash table用,並且與連線鍵值存放在一起
第4步:對build input建立點陣圖向量
第5步:如果記憶體中沒有空間了,則將分割槽寫至磁碟上
第6步:讀取小表S的剩餘部分,重複第三步,直至小表S全部讀完
第7步:將分割槽按大小排序,選取幾個分割槽建立hash table(這裡選取分割槽的原則是使選取的數量最多)
第8步:根據前面用hash_fun_2函式計算好的hash值,建立hash table
第9步:讀取表B,採用點陣圖向量進行點陣圖向量過濾
第10步:對通過過濾的資料採用hash_fun_1函式將資料對映到相應的分割槽中去,並計算hash_fun_2的hash值
第11步:如果所落的分割槽在記憶體中,則將前面通過hash_fun_2函式計算所得的hash值與記憶體中已存在的hash table做連線
將結果寫致磁碟上。如果所落的分割槽不在記憶體中,則將相應的值與表S相應的分割槽放在一起
第12步:繼續讀取表B,重複第9步,直至表B讀取完畢
第13步:讀取相應的(Si,Bi)做hash連線。在這裡會發生動態角色互換
第14步:如果分割槽過後,最小的分割槽也比記憶體大,則發生nested-loop hash join
㈣ Hash Join的成本
⑴ In-Memory Hash Join
Cost(HJ)=Read(S)+ build hash table in memory(CPU)+Read(B) + Perform In memory Join(CPU)
忽略cpu的時間,則:
Cost(HJ)=Read(S)+Read(B)
⑵ On-Disk Hash Join
根據上述的步驟描述,我們可以看出:
Cost(HJ)=Cost(HJ1)+Cost(HJ2)
其中Cost(HJ1)的成本就是掃描S,B表,並將無法放在記憶體上的部分寫回磁碟,對應前面第2步至第12步
Cost(HJ2)即為做nested-loop hash join的成本,對應前面的第13步至第14步
其中Cost(HJ1)近似等於Read(S)+Read(B)+Write((S-M)+(B-B*M/S))
因為在做nested-loop hash join時,對每一chunk的build input,都需要讀取整個probe input,因此
Cost(HJ2)近似等於Read((S-M)+n*(B-B*M/S)),其中n是nested-loop hash join需要迴圈的次數:n=(S/F)/M
一般情況下,如果n大於10的話,hash join的效能將大大下降
從n的計算公式可以看出,n與Fan-out成反比例,提高fan-out,可以降低n
當hash_area_size是固定時,可以降低cluster size來提高fan-out
從這裡我們可以看出,提高hash_multiblock_io_count引數的值並不一定提高hash join的效能
㈤ Hash Join的過程
一次完整的hash join如下:
1 計算小表的分割槽(bucket)數--Hash分桶
決定hash join的一個重要因素是小表的分割槽(bucket)數
這個數字由hash_area_size、hash_multiblock_io_count和db_block_size引數共同決定
Oracle會保留hash area的20%來儲存分割槽的頭資訊、hash點陣圖資訊和hash表
因此,這個數字的計算公式是:
Bucket數=0.8*hash_area_size/(hash_multiblock_io_count*db_block_size)
2 Hash計算
讀取小表資料(簡稱為R),並對每一條資料根據hash演算法進行計算
Oracle採用兩種hash演算法進行計算,計算出能達到最快速度的hash值(第一hash值和第二hash值)
而關於這些分割槽的全部hash值(第一hash值)就成為hash表
3 存放資料到hash記憶體中
將經過hash演算法計算的資料,根據各個bucket的hash值(第一hash值)分別放入相應的bucket中
第二hash值就存放在各條記錄中
4 建立hash點陣圖
與此同時,也建立了一個關於這兩個hash值對映關係的hash點陣圖
5 超出記憶體大小部分被移到磁碟
如果hash area被佔滿,那最大一個分割槽就會被寫到磁碟(臨時表空間)上去
任何需要寫入到磁碟分割槽上的記錄都會導致磁碟分割槽被更新
這樣的話,就會嚴重影響效能,因此一定要儘量避免這種情況
2-5一直持續到整個表的資料讀取完畢
6 對分割槽排序
為了能充分利用記憶體,儘量儲存更多的分割槽,Oracle會按照各個分割槽的大小將他們在記憶體中排序
7 讀取大表資料,進行hash匹配
接下來就開始讀取大表(簡稱S)中的資料
按順序每讀取一條記錄,計算它的hash值,並檢查是否與記憶體中的分割槽的hash值一致
如果是,返回join資料
如果記憶體中的分割槽沒有符合的,就將S中的資料寫入到一個新的分割槽中,這個分割槽也採用與計算R一樣的演算法計算出hash值
也就是說這些S中的資料產生的新的分割槽數應該和R的分割槽集的分割槽數一樣。這些新的分割槽被儲存在磁碟(臨時表空間)上
8 完全大表全部資料的讀取
一直按照7進行,直到大表中的所有資料的讀取完畢
9 處理沒有join的資料
這個時候就產生了一大堆join好的資料和從R和S中計算儲存在磁碟上的分割槽
10 二次hash計算
從R和S的分割槽集中抽取出最小的一個分割槽,使用第二種hash函式計算出並在記憶體中建立hash表
採用第二種hash函式的原因是為了使資料分佈性更好
11 二次hash匹配
在從另一個數據源(與hash在記憶體的那個分割槽所屬資料來源不同的)中讀取分割槽資料,與記憶體中的新hash表進行匹配。返回join資料
12 完成全部hash join
繼續按照9-11處理剩餘分割槽,直到全部處理完畢
㈥ Hash Join的模式
Oracle中,Hash Join也有三種模式:optimal,one-pass,multi-pass
⑴ optimal
當驅動結果集生成的hash表全部可以放入PGA的hash area時,稱為optimal,大致過程如下:
① 先根據驅動表,得到驅動結果集
② 在hash area生成hash bulket,並將若干bulket分成一組,成為一個partition,還會生成一個bitmap的列表,每個bulket在上面佔一位
③ 對結果集的join鍵做hash運算,將資料分散到相應partition的bulket中
當運算完成後,如果鍵值唯一性較高的話,bulket裡的資料會比較均勻,也有可能有的桶裡面數據會是空的
這樣bitmap上對應的標誌位就是0,有資料的桶,標誌位會是1
④ 開始掃描第二張表,對jion鍵做hash運算,確定應該到某個partition的某個bulket去探測
探測之前,會看這個bulket的bitmap是否會1,如果為0,表示沒資料,這行就直接丟棄掉
⑤ 如果bitmap為1,則在桶內做精確匹配,判斷OK後,返回資料
這個是最優的hash join,他的成本基本是兩張表的full table scan,在加微量的hash運算
部落格開篇的那幅圖描述的也就是這種情況
⑵ one-pass
如果程序的pga很小,或者驅動表結果集很大,超過了hash area的大小,會怎麼辦?
當然會用到臨時表空間,此時oracle的處理方式稍微複雜點需奧注意上面提到的有個partition的概念
可以這麼理解,資料是經過兩次hash運算的,先確定你的partition,再確定你的bulket
假設hash area小於整個hash table,但至少大於一個partition的size,這個時候走的就是one-pass
當我們生成好hash表後,狀況是部分partition留在記憶體中,其他的partition留在磁碟臨時表空間中
當然也有可能某個partition一半在記憶體,一半在磁碟,剩下的步驟大致如下:
① 掃描第二張表,對join鍵做hash運算,確定好對應的partition和bulket
② 檢視bitmap,確定bulket是否有資料,沒有則直接丟棄
③ 如果有資料,並且這個partition是在記憶體中的,就進入對應的桶去精確匹配,能匹配上,就返回這行資料,否則丟棄
④ 如果partition是在磁碟上的,則將這行資料放入磁碟中暫存起來,儲存的形式也是partition,bulket的方式
⑤ 當第二張表被掃描完後,剩下的是驅動表和探測表生成的一大堆partition,保留在磁碟上
⑥ 由於兩邊的資料都按照相同的hash演算法做了partition和bulket,現在只要成對的比較兩邊partition資料即可
並且在比較的時候,oracle也做了優化處理,沒有嚴格的驅動與被驅動關係
他會在partition對中選較小的一個作為驅動來進行,直到磁碟上所有的partition對都join完
可以發現,相比optimal,他多出的成本是對於無法放入記憶體的partition,重新讀取了一次,所以稱為one-pass
只要你的記憶體保證能裝下一個partition,oracle都會騰挪空間,每個磁碟partition做到one-pass
⑶ multi-pass
這是最複雜,最糟糕的hash join
此時hash area小到連一個partition也容納不下,當掃描好驅動表後
可能只有半個partition留在hash area中,另半個加其他的partition全在磁碟上
剩下的步驟和one-pass比價類似,不同的是針對partition的處理
由於驅動表只有半個partition在記憶體中,探測表對應的partition資料做探測時
如果匹配不上,這行還不能直接丟棄,需要繼續保留到磁碟,和驅動表剩下的半個partition再做join
這裡舉例的是記憶體可以裝下半個partition,如果裝的更少的話,反覆join的次數將更多
當發生multi-pass時,partition物理讀的次數會顯著增加
㈦ Hash Join的點陣圖
這個位圖包含了每個hash分割槽是否有有值的資訊。它記錄了有資料的分割槽的hash值
這個點陣圖的最大作用就是,如果probe input中的資料沒有與記憶體中的hash表匹配上
先檢視這個點陣圖,以決定是否將沒有匹配的資料寫入磁碟
那些不可能匹配到的資料(即點陣圖上對應的分割槽沒有資料)就不再寫入磁碟
㈧ 小結
① 確認小表是驅動表
② 確認涉及到的表和連線鍵分析過了
③ 如果在連線鍵上資料不均勻的話,建議做柱狀圖
④ 如果可以,調大hash_area_size的大小或pga_aggregate_target的值
⑤ Hash Join適合於小表與大表連線、返回大型結果集的連線.
轉自:http://blog.csdn.net/dba_waterbin/article/details/8554550