引用文獻:
http://blog.itpub.net/24984814/viewspace-708159/
http://blog.csdn.net/yongsheng0550/article/details/5452200
http://www.cnblogs.com/RicCC/archive/2007/06/26/796481.html
Join的實現算法有三種,分別是Nested Loops Join,Merge Join,Hash Join。
DB2、SQL Server和Oracle都是使用這三種方式,不過Oracle選擇使用nested loop的條件跟SQL Server有點差別,內存管理機制跟SQL Server不一樣,因此查看執行計划,Oracle中nested loops運用非常多,而merge和hash方式相對較少,SQL Server中,merge跟hash方式則是非常普遍。
一.Nested Loopsb Join
1.定義
Nested Loops也稱為嵌套迭代,它將一個聯接輸入用作外部輸入表(顯示為圖形執行計划中的頂端輸入),將另一個聯接輸入用作內部(底端)輸入表。外部循環逐行消耗外部輸入表。內部循環為每個外部行執行,在內部輸入表中搜索匹配行。最簡單的情況是,搜索時掃描整個表或索引;這稱為單純嵌套循環聯接。如果搜索時使用索引,則稱為索引嵌套循環聯接。如果將索引生成為查詢計划的一部分(並在查詢完成后立即將索引破壞),則稱為臨時索引嵌套循環聯接。偽碼表示如下:
for each row R1 in the outer table
for each row R2 in the inner table
if R1 joins with R2
return (R1, R2)
2.應用場景
適用於outer table(有的地方叫Master table)的記錄集比較少(<10000)而且inner table(有的地方叫Detail table)索引選擇性較好的情況下(inner table要有index)。
inner table被outer table驅動,outer table返回的每一行都要在inner table中檢索到與之匹配的行。當然也可以用ORDERED 提示來改變CBO默認的驅動表,使用USE_NL(table_name1 table_name2)可是強制CBO 執行嵌套循環連接。
cost = outer access cost + (inner access cost * outer cardinality)
3.常用於執行的連接
Nested Loops常執行Inner Join(內部聯接)、Left Outer Join(左外部聯接)、Left Semi Join(左半部聯接)和Left Anti Semi Join(左反半部聯接)邏輯操作。
Nested Loops通常使用索引在內部表中搜索外部表的每一行。根據預計的開銷,Microsoft SQL Server決定是否對外部輸入進行排序來改變內部輸入索引的搜索位置。
將基於所執行的邏輯操作返回所有滿足 Argument 列內的(可選)謂詞的行。
二.Merge Join
1.定義
Merge Join第一個步驟是確保兩個關聯表都是按照關聯的字段進行排序。如果關聯字段有可用的索引,並且排序一致,則可以直接進行Merge Join操作;否則,SQL Server需要先對關聯的表按照關聯字段進行一次排序(就是說在Merge Join前的兩個輸入上,可能都需要執行一個Sort操作,再進行Merge Join)。
兩個表都按照關聯字段排序好之后,Merge Join操作從每個表取一條記錄開始匹配,如果符合關聯條件,則放入結果集中;否則,將關聯字段值較小的記錄拋棄,從這條記錄對應的表中取下一條記錄繼續進行匹配,直到整個循環結束。
在多對多的關聯表上執行Merge Join時,通常需要使用臨時表進行操作。例如A join B使用Merge Join時,如果對於關聯字段的某一組值,在A和B中都存在多條記錄A1、A2...An、B1、B2...Bn,則為A中每一條記錄A1、A2...An,都必須在B中對所有相等的記錄B1、B2...Bn進行一次匹配。這樣,指針需要多次從B1移動到Bn,每一次都需要讀取相應的B1...Bn記錄。將B1...Bn的記錄預先讀出來放入內存臨時表中,比從原數據頁或磁盤讀取要快。
2.應用場景另
用在數據沒有索引但是已經排序的情況下。
通常情況下hash join的效果都比Sort merge join要好,然而如果行源已經被排過序,在執行排序合並連接時不需要再排序了,這時Sort merge join的性能會優於hash join。可以使用USE_MERGE(table_name1 table_name2)來強制使用Sort merge join。
cost = (outer access cost * # of hash partitions) + inner access cost
3.常用於執行的連接
Merge Join常執行Inner Join(內部聯接)、Left Outer Join(左外部聯接)、Left Semi Join(左半部聯接)、Left Anti Semi Join(左反半部聯接)、Right Outer Join(右外部聯接)、Right Semi Join(右半部聯接)、Right Anti Semi Join(右反半部聯接)和Union(聯合)邏輯操作。
在 Argument 列中,如果操作執行一對多聯接,則 Merge Join 運算符將包含 MERGE:() 謂詞;如果操作執行多對多聯接,則該運算符將包含 MANY-TO-MANY MERGE:() 謂詞。Argument 列還包含一個用於執行操作的列的列表,該列表以逗號分隔。Merge Join 運算符要求在各自的列上對兩個輸入進行排序,這可以通過在查詢計划中插入顯式排序操作來實現。如果不需要顯式排序(例如,如果數據庫內有合適的 B 樹索引或可以對多個操作(如合並聯接和對匯總分組)使用排序順序),則合並聯接尤其有效。
三.Hash Join
1.定義
Hash Match有兩個輸入:build input(也叫做outer input)和probe input(也叫做inner input),不僅用於inner/left/right join等,象union/group by等也會使用hash join進行操作,在group by中build input和probe input都是同一個記錄集。
Hash Match操作分兩個階段完成:Build(構造)階段和Probe(探測)階段。
Build(構造)階段主要構造哈希表(hash table)。在inner/left/right join等操作中,表的關聯字段作為hash key;在group by操作中,group by的字段作為hash key;在union或其它一些去除重復記錄的操作中,hash key包括所有的select字段。
Build操作從build input輸入中取出每一行記錄,將該行記錄關聯字段的值使用hash函數生成hash值,這個hash值對應到hash table中的hash buckets(哈希表目)。如果一個hash值對應到多個hash buckts,則這些hash buckets使用鏈表數據結構連接起來。當整個build input的table處理完畢后,build input中的所有記錄都被hash table中的hash buckets引用/關聯了。
Probe(探測)階段,SQL Server從probe input輸入中取出每一行記錄,同樣將該行記錄關聯字段的值,使用build階段中相同的hash函數生成hash值,根據這個hash值,從build階段構造的hash table中搜索對應的hash bucket。hash算法中為了解決沖突,hash bucket可能會鏈接到其它的hash bucket,probe動作會搜索整個沖突鏈上的hash bucket,以查找匹配的記錄。
如果build input記錄數非常大,構建的hash table無法在內存中容納時,SQL Server分別將build input和probe input切分成多個分區部分(partition),每個partition都包括一個獨立的、成對匹配的build input和probe input,這樣就將一個大的hash join切分成多個獨立、互相不影響的hash join,每一個分區的hash join都能夠在內存中完成。SQL Server將切分后的partition文件保存在磁盤上,每次裝載一個分區的build input和probe input到內存中,進行一次hash join。這種hash join叫做Grace Hash join,使用的Grace Hash Join算法。
2.應用場景
適用於兩個表的數據量差別很大。但需要注意的是:如果HASH表太大,無法一次構造在內存中,則分成若干個partition,寫入磁盤的temporary segment,則會多一個I/O的代價,會降低效率,此時需要有較大的temporary segment從而盡量提高I/O的性能。
可以用USE_HASH(table_name1 table_name2)提示來強制使用散列連接。如果使用散列連HASH_AREA_SIZE 初始化參數必須足夠的大,如果是9i,Oracle建議使用SQL工作區自動管理,設置WORKAREA_SIZE_POLICY 為AUTO,然后調整PGA_AGGREGATE_TARGET 即可。
也可以使用HASH_JOIN_ENABLED=FALSE(默認為TRUE)強制不使用hash join。
cost = (outer access cost * # of hash partitions) + inner access cost
3.常用於執行的鏈接
Hash Match運算符通過計算其生成輸入中每行的哈希值生成哈希表。HASH:()謂詞以及一個用於創建哈希值的列的列表出現在Argument列內。然后,該謂詞為每個探測行(如果適用)使用相同的哈希函數計算哈希值並在哈希表內查找匹配項。如果存在殘留謂詞(由 Argument 列中的 RESIDUAL:() 標識),則還須滿足此殘留謂詞,只有這樣行才能被視為是匹配項。行為取決於所執行的邏輯操作:
(1)對於聯接,使用第一個(頂端)輸入生成哈希表,使用第二個(底端)輸入探測哈希表。按聯接類型規定的模式輸出匹配項(或不匹配項)。如果多個聯接使用相同的聯接列,這些操作將分組為一個哈希組。
(2)對於非重復或聚合運算符,使用輸入生成哈希表(刪除重復項並計算聚合表達式)。生成哈希表時,掃描該表並輸出所有項。
(3)對於 union 運算符,使用第一個輸入生成哈希表(刪除重復項)。使用第二個輸入(它必須沒有重復項)探測哈希表,返回所有沒有匹配項的行,然后掃描該哈希表並返回所有項。
四.性能分析
Hash join的主要資源消耗在於CPU(在內存中創建臨時的hash表,並進行hash計算),而merge join的資源消耗主要在於磁盤I/O(掃描表或索引)。在並行系統中,hash join對CPU的消耗更加明顯。所以在CPU緊張時,最好限制使用hash join。
在絕大多數情況下,hash join效率比其他join方式效率更高:
在Sort-Merge Join(SMJ),兩張表的數據都需要先做排序,然后做merge。因此效率相對最差;
Nested-Loop Join(NL)效率比SMJ更高。特別是當驅動表的數據量很大(集的勢高)時。這樣可以並行掃描內表。
Hash join效率最高,因為只要對兩張表掃描一次
Merge Join(合並聯接)本身的速度很快,但如果需要排序操作,選擇合並聯接就會非常費時。然而,如果數據量很大且能夠從現有 B 樹索引中獲得預排序的所需數據,則合並聯接通常是最快的可用聯接算法。如果是無序的數據,Merge Join首先做的是排序,如果數據量大,排序就會溢出到tempdb, 效率就將低了。
如果外部輸入很小(<10000)而內部輸入很大且預先創建了索引,則Nested Loops(嵌套循環聯接)尤其有效。在許多小事務中(如那些只影響較小的一組行的事務),索引嵌套循環聯接遠比合並聯接和哈希聯接優越。但在大查詢中,嵌套循環聯接通常不是最佳選擇。
如果兩個表的數據量差別很大,則使用Hash Match。但需要注意的是:如果HASH表太大,無法一次構造在內存中,則分成若干個partition,寫入磁盤的temporary segment,則會多一個I/O的代價,會降低效率,此時需要有較大的temporary segment從而盡量提高I/O的性能。Hash join的主要資源消耗在於CPU(在內存中創建臨時的HASH表,並進行HASH計算),而Merge join的資源消耗主要在於磁盤I/O(掃描表或索引)。
五.優化原則
1.若有單行謂詞,則他的表一定是驅動表(select * from employees e,departments d where e.department_id=d.department_id and e.department_id=100 and salary=10000; 上面的語句中e.department_id=d.department_id是連接謂詞,e.department_id=100是非連接謂詞(對連接列的限制),salary=10000是單行謂詞(對非連接列的限制))
2.外連接時,一定是用顯示的行數比較多的那個表作為驅動表。如:
select e.employee_id,e.department_id,d.manager_id,d.location_id from employees e right join departments d on e.department_id=d.department_id
則departments表顯示的行數一定大於等於employees表,所以應該要以departments表作為驅動表,如果以employees表作為驅動表,則departments表中多顯示的那幾行就顯示不出來了
4.一般情況下,Hash Join處理代價非常高,是數據庫服務器內存和CPU的頭號殺手之一,尤其是涉及到分區(數據量太大導致內存不夠的情況,或者並發訪問很高導致當前處理線程無法獲得足夠的內存,那么數據量不是特大的情況下也可能需要進行分區),為了盡快的完成所有的分區步驟,將使用大量異步的I/O操作,因此期間單一一個線程就可能導致多個磁盤驅動器出於忙碌狀態,這很有可能阻塞其它線程的執行。 因此,
5. 要避免大數據的Hash Join,盡量將其轉化為高效的Merge Join、Nested Loops。可能使用的手段有表結構設計、索引調整設計、SQL優化,以及業務設計優化。例如冗余字段的運用,將統計分析結果用service定期跑到靜態表中,適當的冗余表,使用AOP或類似機制同步更新等。
6. 盡量減少join兩個輸入端的數據量。這一點比較常犯的毛病是,條件不符合SARG((Searchable Arguments),在子查詢內部條件給的不充分(SQL過於復雜情況下SQL Server查詢優化器經常犯傻,寫在子查詢外部的條件不會被用在子查詢內部,影響子查詢內部的效率或者是跟子查詢再join時候的效率)。