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,以查找匹配的記錄。
關於hash算法的細節,可以查看數據結構的一些資料。hash算法主要是用於大數據量的搜索,為了避免每次都象merge join一樣在全部的數據中進行搜索匹配,通過合適的 hash函數,先給要搜索的數據根據hash key建立hash值作為索引,在搜索時,先通過hash值定位到一個較小的搜索范圍,然後在這個范圍中搜索匹配符合條件的結果,以提高效率。
SQL Server將數據量較小的表作為build input,盡量使根據build input構造的hash table能夠完全放在內存中,這樣probe階段的匹配操作就完全是在內存中進行,這樣的hash join叫做In-Memory Hash join。
如果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算法。
伴隨著大數據的hash join運算,還會有standard external merge sorts、multiple merge levels、multiple partitioning steps、multiple partitioning levels,SQL Server還可能會使用Recursive Hash Join等算法或其它的優化手段。
hash join一般都用於大數據量的操作,例如join中某個表的數據達到一定程度或者無法一次加載到內存,另外如果你的關聯字段在兩個join表中都不能夠命中索引,也是使用hash join來處理。因此一般情況下,hahs join處理代價非常高,是數據庫服務器內存和CPU的頭號殺手之一,尤其是涉及到分區(數據量太大導致內存不夠的情況,或者並發訪問很高導致當前處理線程無法獲得足夠的內存,那麼數據量不是特大的情況下也可能需要進行分區),為了盡快的完成所有的分區步驟,將使用大量異步的I/O操作,因此期間單一一個線程就可能導致多個磁盤驅動器出於忙碌狀態,這很有可能阻塞其它線程的執行。
使用inner hash join或者option (hash join)強制使用hash join方法。
建議
三種join方法,都是擁有兩個輸入。優化的基本原則:1. 避免大數據的hash join,盡量將其轉化為高效的merge join、nested loop join。可能使用的手段有表結構設計、索引調整設計、SQL優化,以及業務設計優化。例如冗余字段的運用,將統計分析結果用service定期跑到靜態表中,適當的冗余表,使用AOP或類似機制同步更新等。2. 盡量減少join兩個輸入端的數據量。這一點比較常犯的毛病是,條件不符合SARG(光這一點就有很多高超的技巧可以發揮),在子查詢內部條件給的不充分(SQL過於復雜情況下SQL Server查詢優化器經常犯傻,寫在子查詢外部的條件不會被用在子查詢內部,影響子查詢內部的效率或者是跟子查詢再join時候的效率)。另外也是設計、業務端盡量限制這兩個輸入的數據量了。
關於業務設計方面的優化,參考以前寫的一篇post:系統分析設計 一個JOIN問題解決方案的感想 重視業務分析設計。
補充:關於SQL Server 2005
大致看了下SQL Server 2005,執行計劃的顯示確實有一些不一樣,但主要部分或者說原理上是差不多的,不會有多少偏差。上面的示例SQL,在tableB上面使用非聚集索引時,SQL Server 2005的執行計劃。
一個主要的不同點是SQL Server 2000下面Bookmark Lookup操作,在2005下面顯示成一個RID Lookup操作 + 一個Nested Loops操作實現,其實這也是很好理解的,可以說這樣顯示執行計劃更合理一點,讓你一看到這個操作,就知道它是通過一個循環機制到tableB中獲取實際數據。
另外一點是,將鼠標移動到執行計劃的圖標上面後,彈出的提示信息的一些改變,例如2005裡面會顯示每個操作的輸出列表(output list),而我上面的文章中基本都使用“輸出數據結構”這樣一個詞匯在表達。通過查看output list,你更能明白RID Lookup(Bookmark Lookup)這樣的操作存在的理由了。
最後,2005裡面可以將圖形顯示的執行計劃保存下來,以後可以打開再以圖形方式進行查看分析,這個在2000下面是不行的,2000只能保存執行計劃的文本。這樣一些小功能對於分析SQL性能非常有用,在圖形界面上的分析更直觀。