1.概述
hash join是一種數據庫在進行多表連接時的處理算法,對於多表連接還有兩種比較常用的方式:sort merge-join 和 nested loop。 為了比較清楚的介紹hash join的使用場景以及為何要引入這樣一種連接算法,這里也會順帶簡單介紹一下上面提到的兩種join方式。
連接方式是一個什么樣的概念,或者說我們為何要有而且有好幾種,對於不太了解數據庫的人來講可能這些是開頭的疑惑。簡單來講,我們將數據存在不同的表中,而不同的表有着它們自身的表結構,不同表之間可以是有關聯的,大部分實際使用中,不會僅僅只需要一張表的信息,比如需要從一個班級表中找出杭州地區的學生,再用這個信息去檢索成績表中他們的數學成績,如果沒有多表連接,那只能手動將第一個表的信息查詢出來作為第二個表的檢索信息去查詢最終的結果,可想而知這將會是多么繁瑣。
對於幾個常見的數據庫,像oracle,postgresql它們都是支持hash-join的,mysql並不支持。在這方面,oracle和pg都已經做的比較完善了,hash-join本身的實現並不是很復雜,但是它需要優化器的實現配合才能最大的發揮本身的優勢,我覺得這才是最難的地方。
多表連接的查詢方式又分為以下幾種:內連接,外連接和交叉連接。外連接又分為:左外連接,右外連接和全外連接。對於不同的查詢方式,使用相同的join算法也會有不同的代價產生,這個是跟其實現方式緊密相關的,需要考慮不同的查詢方式如何實現,對於具體使用哪一種連接方式是由優化器通過代價的衡量來決定的,后面會簡單介紹一下幾種連接方式代價的計算。 hashjoin其實還有很多需要考慮和實現的地方,比如數據傾斜嚴重如何處理、內存放不下怎木辦,hash如何處理沖突等,這些並不是本文介紹的重點,不再詳述,每個拿出來都可以再講一篇了。
nested loop join
嵌套循環連接,是比較通用的連接方式,分為內外表,每掃描外表的一行數據都要在內表中查找與之相匹配的行,沒有索引的復雜度是O(N*M),這樣的復雜度對於大數據集是非常劣勢的,一般來講會通過索引來提升性能。
sort merge-join
merge join需要首先對兩個表按照關聯的字段進行排序,分別從兩個表中取出一行數據進行匹配,如果合適放入結果集;不匹配將較小的那行丟掉繼續匹配另一個表的下一行,依次處理直到將兩表的數據取完。merge join的很大一部分開銷花在排序上,也是同等條件下差於hash join的一個主要原因。
2.原理和實現
簡單的對於兩個表來講,hash-join就算講兩表中的小表(稱S)作為hash表,然后去掃描另一個表(稱M)的每一行數據,用得出來的行數據根據連接條件去映射建立的hash表,hash表是放在內存中的,這樣可以很快的得到對應的S表與M表相匹配的行。
對於結果集很大的情況,merge-join需要對其排序效率並不會很高,而nested loop join是一種嵌套循環的查詢方式無疑更不適合大數據集的連接,而hash-join正是為處理這種棘手的查詢方式而生,尤其是對於一個大表和一個小表的情況,基本上只需要將大小表掃描一遍就可以得出最終的結果集。
不過hash-join只適用於等值連接,對於>, <, <=, >=這樣的查詢連接還是需要nested loop這種通用的連接算法來處理。如果連接key本來就是有序的或者需要排序,那么可能用merge-join的代價會比hash-join更小,此時merge-join會更有優勢。
好了,廢話說了不少了,來講講實現,拿一條簡單的多表sql查詢語句來舉個栗子:select * from t1 join t2 on t1.c1 = t2.c1 where t1.c2 > t2.c2 and t1.c1 > 1。這樣一條sql進入數據庫系統中,它是如何被處理和解剖的呢?sql:鬼知道我都經歷了些什么。。。
1.背景知識
1.第一步呢,它需要經歷詞法以及語法的解析,這部分的輸出是一顆帶有token結點的語法樹。
語法分析,顧名思義這部分只是語法層面的剖析,將一個string的sql語句處理成為一顆有着雛形結構的node tree,每個結點有它們自身的特殊標識,但是並沒有分析和處理這個結點的具體含義和值。
2. 第二步是語義分析和重寫處理。
重寫的過程不同的數據庫可能有不同的處理,有些可能是跟邏輯執行過程放在一起,有的則分開。
這一步做完樹的形狀大體上是與語法分析樹保持一致的,但是此時的結點都攜帶了一些具體的信息,以where后面的表達式為例,這顆中綴表達式每一個結點都有了自身的類型和特定的信息,並不關心值是什么,這步做完后進入改寫過程,改寫是一種邏輯優化方式,使得一些復雜的sql語句變得更簡單或者更符合數據庫的處理流程。
3.優化器處理
優化器的處理是比較復雜的,也是sql模塊最難的地方,優化無止境,所以優化器沒有最優只有更優。優化器需要考慮方方面面的因素既要做的通用型很強又要保證很強的優化能力和正確性。
優化器最重要的作用莫過於路徑選擇了,對於多表連接如何確定表連接的順序和連接方式,不同的數據庫有着不同的處理方式,pg支持動態規划算法,表數量過多的時候使用遺傳算法。路徑的確定又依賴於代價模型的實現,代價模型會維護一些統計信息,像列的最大值、最小值、NDV和DISTINCT值等,通過這些信息可以計算選擇率從而進一步計算代價。
回歸到正文,使用哪一種連接方式就是在這里決定的,hash join 對大小表是有要求的,所以這里形成的計划是t1-t2還是t2-t1是不一樣的,每種連接方式有着自身的代價計算方式。
hash join的代價估算:
COST = BUILD_COST + M_SCAN_COST + JOIN_CONDITION_COST + FILTER_COST
簡單來看,hash join的代價主要在於建立hash表、掃描M表、join條件連接和filter過濾,對於S表和M表都是只需要掃描一次即可,filter過濾是指t1.c2>t2.c2這樣的條件過濾,對於t1.c1>1這樣只涉及單表的條件會被下壓,在做連接之前就被過濾了。
優化器處理過后,會生成一顆執行計划樹,真正的實現過程根據執行計划的流程操作數據,由低向上地遞歸處理並返回數據。
2.hash join的實現
hash join的實現分為build table也就是被用來建立hash map的小表和probe table,首先依次讀取小表的數據,對於每一行數據根據連接條件生成一個hash map中的一個元組,數據緩存在內存中,如果內存放不下需要dump到外存。依次掃描探測表拿到每一行數據根據join condition生成hash key映射hash map中對應的元組,元組對應的行和探測表的這一行有着同樣的hash key, 這時並不能確定這兩行就是滿足條件的數據,需要再次過一遍join condition和filter,滿足條件的數據集返回需要的投影列。
hash join實現的幾個細節
1.hash join本身的實現不要去判斷哪個是小表,優化器生成執行計划時就已經確定了表的連接順序,以左表為小表建立hash table,那對應的代價模型就會以左表作為小表來得出代價,這樣根據代價生成的路徑就是符合實現要求的。
2.hash table的大小、需要分配多少個桶這個是需要在一開始就做好的,那分配多少是一個問題,分配太大會造成內存浪費,分配太小會導致桶數過小開鏈過長性能變差,一旦超過這里的內存限制,會考慮dump到外存,不同數據庫有它們自身的實現方式。
3.如何對數據hash,不同數據庫有着自己的方式,不同的哈希方法也會對性能造成一定的影響。