SQL夯實基礎(八):聯接運算符算法歸類

   今天主要介紹三個經常使用聯接運算符算法:合併聯接(Merge join),哈希聯接(Hash Join)和嵌套循環聯接(Nested Loop Join)。(mysql至8.0版本,都只支持Nested Loop Join,下一篇文章我會單獨說下mysql對Nested Loop Join的使用)mysql

一個關係能夠是:算法

1 一個表sql

2 一個索引緩存

3 上一個運算的中間結果(好比上一個聯接運算的結果)多線程

  當你聯接兩個關係時,聯接算法對兩個關係的處理是不一樣的。在本文剩餘部分,我將假定:函數

外關係是左側數據集(驅動表),內關係是右側數據集(非驅動表、被驅動表)。oop

好比, A JOIN B 是 A 和 B 的聯接,這裏 A 是外關係,B 是內關係。多數狀況下,A JOIN B 的成本跟 B JOIN A 的成本是不一樣的優化

在這一部分,我還將假定外關係有 N 個元素,內關係有 M 個元素。要記住,真實的優化器經過統計知道 N 和 M 的值spa

注:N 和 M 是關係的基數。【基數線程

驅動表與被驅動表

  這裏咱們叫他外關係和內關係(由於不少文章對這兩個概念有不一樣的命名,很容易混亂):

  驅動表,即須要從驅動表中拿出來每條記錄,去與被驅動表的全部記錄進行匹配探測。

  理解驅動表和被驅動表的差別,最本質的問題,須要理解順序讀取和隨機讀取的差別,內存是適合隨機讀取的,可是硬盤就不是(固態隨機讀稍微好些,可是對比順序讀有差距),對於硬盤來講順序讀取的效率比較好。

一、驅動表,做爲外層循環,若能只進行一次IO把全部數據拿出來最好,這就比較適合順序讀取,一次性批量的把數據讀取出來,這裏沒考慮緩存等細節。

二、被驅動表,即裏層循環,因爲須要不斷的拿外層循環傳進來的每條記錄去匹配,因此若是是適合隨機讀取的,那麼效率就會比較高。若是表上有索引,實際上就意味着這個表是適合隨機讀取的。若是表的數據量較大,且沒有索引,那麼就不適合屢次的隨機讀取,比較適合一次性的批量讀取,就應該做爲驅動表。

嵌套循環聯接

嵌套循環聯接是最簡單的。

 

道理以下:

1 針對外關係的每一行

2 查看內關係裏的全部行來尋找匹配的行

下面是僞代碼:

nested_loop_join(array outer, array inner)
  for each row a in outer
    for each row b in inner
      if (match_join_condition(a,b))
        write_result_in_output(a,b)
      end if
    end for
   end for

因爲這是個雙迭代,時間複雜度是 O(N*M)

  在磁盤 I/O 方面, 針對 N 行外關係的每一行,內部循環須要從內關係讀取 M 行。這個算法須要從磁盤讀取 N+ N*M 行。可是,若是內關係足夠小,你能夠把它讀入內存,那麼就只剩下 M + N 次讀取。這樣修改以後,內關係必須是最小的,由於它有更大機會裝入內存

在CPU成本方面沒有什麼區別,可是在磁盤 I/O 方面,最好的是每一個關係只讀取一次。

固然,內關係能夠由索引代替,對磁盤 I/O 更有利

因爲這個算法很是簡單,下面這個版本在內關係太大沒法裝入內存時,對磁盤 I/O 更加有利。道理以下:

1 爲了不逐行讀取兩個關係,

2 你能夠成簇讀取,把(兩個關係裏讀到的)兩簇數據行保存在內存裏,

3 比較兩簇數據,保留匹配的,

4 而後從磁盤加載新的數據簇來繼續比較

5 直到加載了全部數據。

可能的算法以下:

// improved version to reduce the disk I/O.

nested_loop_join_v2(file outer, file inner)

  for each bunch ba in outer

  // ba is now in memory

    for each bunch bb in inner

        // bb is now in memory

        for each row a in ba

          for each row b in bb

            if (match_join_condition(a,b))

              write_result_in_output(a,b)

            end if

          end for

       end for

    end for

   end for

使用這個版本,時間複雜度沒有變化,可是磁盤訪問下降了:

1 用前一個版本,算法須要 N + N*M 次訪問(每次訪問讀取一行)。

2 用新版本,磁盤訪問變爲外關係的數據簇數量 + 外關係的數據簇數量 * 內關係的數據簇數量。

3 增長數據簇的尺寸,能夠下降磁盤訪問

哈希聯接

哈希聯接更復雜,不過在不少場合比嵌套循環聯接成本低。

 

哈希聯接的道理是:

1) 讀取內關係的全部元素

2) 在內存裏建一個哈希表

3) 逐條讀取外關係的全部元素

4) (用哈希表的哈希函數)計算每一個元素的哈希值,來查找內關係裏相關的哈希桶內

5) 是否與外關係的元素匹配。

  在時間複雜度方面我須要作些假設來簡化問題:

1 內關係被劃分紅 X 個哈希桶

2 哈希函數幾乎均勻地分佈每一個關係內數據的哈希值,就是說哈希桶大小一致。

3 外關係的元素與哈希桶內的全部元素的匹配,成本是哈希桶內元素的數量。

  時間複雜度是 (M/X) * N + 建立哈希表的成本(M) + 哈希函數的成本 * N。若是哈希函數建立了足夠小規模的哈希桶,那麼複雜度就是 O(M+N)。

  還有個哈希聯接的版本,對內存有利可是對磁盤 I/O 不夠有利。 這回是這樣的:

1) 計算內關係和外關係雙方的哈希表

2) 保存哈希表到磁盤

3) 而後逐個哈希桶比較(其中一個讀入內存,另外一個逐行讀取)。

合併聯接

合併聯接是惟一產生排序的聯接算法

注:這個簡化的合併聯接不區份內表或外表;兩個表扮演一樣的角色。可是真實的實現方式是不一樣的,好比當處理重複值時。

1.(可選)排序聯接運算:兩個輸入源都按照聯接關鍵字排序。

2.合併聯接運算:排序後的輸入源合併到一塊兒。

  咱們已經談到過合併排序,在這裏合併排序是個很好的算法(可是並不是最好的,若是內存足夠用的話,仍是哈希聯接更好)。

然而有時數據集已經排序了,好比:

1  若是表內部就是有序的,好比聯接條件裏一個索引組織表

2  若是關係是聯接條件裏的一個索引

3  若是聯接應用在一個查詢中已經排序的中間結果 

  這部分與咱們研究過的合併排序中的合併運算很是類似。不過這一次呢,咱們不是從兩個關係裏挑選全部元素,而是隻挑選相同的元素。道理以下:

1) 在兩個關係中,比較當前元素(當前=頭一次出現的第一個)

2) 若是相同,就把兩個元素都放入結果,再比較兩個關係裏的下一個元素

3) 若是不一樣,就去帶有最小元素的關係裏找下一個元素(由於下一個元素可能會匹配)

4) 重複 一、二、3步驟直到其中一個關係的最後一個元素。

由於兩個關係都是已排序的,你不須要『回頭去找』,因此這個方法是有效的。

該算法是個簡化版,由於它沒有處理兩個序列中相同數據出現屢次的狀況(即多重匹配)。真實版本『僅僅』針對本例就更加複雜,因此我才選擇簡化版。

1 若是兩個關係都已經排序,時間複雜度是 O(N+M)

2 若是兩個關係須要排序,時間複雜度是對兩個關係排序的成本:O(N*Log(N) + M*Log(M))

哪一個算法最好?

若是有最好的,就不必弄那麼多種類型了。這個問題很難,由於不少因素都要考慮,好比:

1 空閒內存:沒有足夠的內存的話就跟強大的哈希聯接拜拜吧(至少是徹底內存中哈希聯接)。

2 兩個數據集的大小。好比,若是一個大表聯接一個很小的表,那麼嵌套循環聯接就比哈希聯接快,由於後者有建立哈希的高昂成本;若是兩個表都很是大,那麼嵌套循環聯接CPU成本就很高昂。

3 是否有索引:有兩個 B+樹索引的話,聰明的選擇彷佛是合併聯接

4 結果是否須要排序:即便你用到的是未排序的數據集,你也可能想用成本較高的合併聯接(帶排序的),由於最終獲得排序的結果後,你能夠把它和另外一個合併聯接串起來(或者也許由於查詢用 ORDER BY/GROUP BY/DISTINCT 等操做符隱式或顯式地要求一個排序結果)。

5 關係是否已經排序:這時候合併聯接是最好的候選項。

6 聯接的類型:是等值聯接(好比 tableA.col1 = tableB.col2 )? 仍是內聯接?外聯接?笛卡爾乘積?或者自聯接?有些聯接在特定環境下是沒法工做的。

7 數據的分佈:若是聯接條件的數據是傾斜的(好比根據姓氏來聯接人,可是不少人同姓),用哈希聯接將是個災難,緣由是哈希函數將產生分佈極不均勻的哈希桶

8 若是你但願聯接操做使用多線程或多進程。

相關文章
相關標籤/搜索