1. 程式人生 > >深入理解Oracle表(5):三大表連線方式詳解之Hash Join的定義,原理,演算法,成本,模式和點陣圖

深入理解Oracle表(5):三大表連線方式詳解之Hash Join的定義,原理,演算法,成本,模式和點陣圖

 Hash Join只能用於相等連線,且只能在CBO優化器模式下。相對於nested loop join,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?
  1. [email protected]> ed  
  2. Wrote file afiedt.buf  
  3.   1  select a.ksppinm name,b.ksppstvl value,a.ksppdesc description  
  4.   2  from x$ksppi a,x$ksppcv b  
  5.   3  where a.indx = b.indx  
  6.   4* and a.ksppinm like'%hash_multiblock_io_count%'
  7. [email protected]> /  
  8. NAME                           VALUE DESCRIPTION  
  9. ------------------------------ ----- ------------------------------------------------------------
  10. _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