首頁 > 軟體

Doris Join 優化原理檔案詳解

2022-10-28 14:04:10

Doris Join 優化原理

Doris 支援兩種物理運算元,一類是 Hash Join,另一類是 Nest Loop Join。

  • Hash Join:在右表上根據等值 Join 列建立雜湊表,左表流式的利用雜湊表進行 Join 計算,它的限制是只能適用於等值 Join。
  • Nest Loop Join:通過兩個 for 迴圈,很直觀。然後它適用的場景就是不等值的 Join,例如:大於小於或者是需要求笛卡爾積的場景。它是一個通用的 Join 運算元,但是效能表現差。

作為分散式的 MPP 資料庫, 在 Join 的過程中是需要進行資料的 Shuffle。資料需要進行拆分排程,才能保證最終的 Join 結果是正確的。舉個簡單的例子,假設關係S 和 R 進行Join,N 表示參與 Join 計算的節點的數量;T 則表示關係的 Tuple 數目。

Doris Shuffle 方式

Doris 支援 4 種 Shuffle 方式

Broadcast Join

它要求把右表全量的資料都傳送到左表上,即每一個參與 Join 的節點,它都擁有右表全量的資料,也就是 T(R)。

它適用的場景是比較通用的,同時能夠支援 Hash Join 和 Nest loop Join,它的網路開銷 N * T(R)。

左表資料不移動,右表資料傳送到左表資料的掃描節點。

Shuffle Join

當進行 Hash Join 時候,可以通過 Join 列計算對應的 Hash 值,並進行 Hash 分桶。

它的網路開銷則是:T(R) + T(N),但它只能支援 Hash Join,因為它是根據 Join 的條件也去做計算分桶的。

左右表資料根據分割區,計算的記過傳送到不同的分割區節點上。

Bucket Shuffle Join

Doris 的表資料本身是通過 Hash 計算分桶的,所以就可以利用表本身的分桶列的性質來進行 Join 資料的 Shuffle。假如兩張表需要做 Join,並且 Join 列是左表的分桶列,那麼左表的資料其實可以不用去移動右表通過左表的資料分桶傳送資料就可以完成 Join 的計算。

它的網路開銷則是:T(R)相當於只 Shuffle 右表的資料就可以了。

左表資料不移動,右表資料根據分割區計算的結果傳送到左表掃表的節點

Colocate

它與 Bucket Shuffle Join 相似,相當於在資料匯入的時候,根據預設的 Join 列的場景已經做好了資料的 Shuffle。那麼實際查詢的時候就可以直接進行 Join 計算而不需要考慮資料的 Shuffle 問題了。

資料已經預先分割區,直接在本地進行 Join 計算

四種 Shuffle 方式對比

Shuffle方式網路開銷物理運算元適用場景
BroadCastN * T(R)Hash Join / Nest Loop Join通用
ShuffleT(S) + T(R)Hash Join通用
Bucket ShuffleT(R)Hash JoinJoin條件中存在左表的分散式列,且左表執行時為單分割區
Colocate0Hash JoinJoin條件中存在左表的分散式列,切左右表同屬於一個Colocate Group

N : 參與 Join 計算的 Instance 個數

T(關係) : 關係的 Tuple 數目

上面這 4 種方式靈活度是從高到低的,它對這個資料分佈的要求是越來越嚴格,但 Join 計算的效能也是越來越好的。

Runtime Filter Join 優化

Doris 在進行 Hash Join 計算時會在右表構建一個雜湊表,左表流式的通過右表的雜湊表從而得出 Join 結果。而 RuntimeFilter 就是充分利用了右表的 Hash 表,在右表生成雜湊表的時,同時生成一個基於雜湊表資料的一個過濾條件,然後下推到左表的資料掃描節點。通過這樣的方式,Doris 可以在執行時進行資料過濾。

假如左表是一張大表,右表是一張小表,那麼利用右表生成的過濾條件就可以把絕大多數在 Join 層要過濾的資料在資料讀取時就提前過濾,這樣就能大幅度的提升 Join 查詢的效能。

當前 Doris 支援三種型別 RuntimeFilter

  • 一種是 IN,很好理解,將一個 hashset 下推到資料掃描節點。
  • 第二種就是 BloomFilter,就是利用雜湊表的資料構造一個 BloomFilter,然後把這個 BloomFilter 下推到查詢資料的掃描節點。。
  • 最後一種就是 MinMax,就是個 Range 範圍,通過右表資料確定 Range 範圍之後,下推給資料掃描節點。

Runtime Filter 適用的場景有兩個要求:

  • 第一個要求就是左表大右表小,因為構建 Runtime Filter是需要承擔計算成本的,包括一些記憶體的開銷。
  • 第二個要求就是左右表 Join 出來的結果很少,說明這個 Join 可以過濾掉左表的絕大部分資料。

當符合上面兩個條件的情況下,開啟 Runtime Filter 就能收穫比較好的效果

當 Join 列為左表的 Key 列時,RuntimeFilter 會下推到儲存引擎。Doris 本身支援延遲物化,

延遲物化簡單來說是這樣的:假如需要掃描 A、B、C 三列,在 A 列上有一個過濾條件: A 等於 2,要掃描 100 行的話,可以先把 A 列的 100 行掃描出來,再通過 A = 2 這個過濾條件過濾。之後通過過濾完成後的結果,再去讀取 B、C 列,這樣就能極大的降低資料的讀取 IO。所以說 Runtime Filter 如果在 Key 列上生成,同時利用 Doris 本身的延遲物化來進一步提升查詢的效能。

Runtime Filter 型別

Doris 提供了三種不同的 Runtime Filter 型別:

  • IN 的優點就是效果過濾效果明顯,且快速。它的缺點首先第一個它只適用於 BroadCast,第二,它右表超過一定資料量的時候就失效了,當前 Doris 目前設定的是1024,即右表如果大於 1024,IN 的 Runtime Filter 就直接失效了。
  • MinMax 的優點是開銷比較小。它的缺點就是對數值列還有比較好的效果,但對於非數值列,基本上就沒什麼效果。
  • Bloom Filter 的特點就是通用,適用於各種型別、效果也比較好。缺點就是它的設定比較複雜並且計算較高。

Join Reorder

資料庫一旦涉及到多表 Join,Join 的順序對整個 Join 查詢的效能是影響很大的。假設有三張表 Join,參考下面這張圖,左邊是 a 表跟 b 張表先做 Join,中間結果的有 2000 行,然後與 c 表再進行 Join 計算。

接下來看右圖,把 Join 的順序調整了一下。把 a 表先與 c 表 Join,生成的中間結果只有 100,然後最終再與 b 表 Join 計算。最終的 Join 結果是一樣的,但是它生成的中間結果有 20 倍的差距,這就會產生一個很大的效能 Diff 了。

Doris 目前支援基於規則的 Join Reorder 演演算法。它的邏輯是:

  • 讓大表、跟小表儘量做 Join,它生成的中間結果是儘可能小的。
  • 把有條件的 Join 表往前放,也就是說盡量讓有條件的 Join 表進行過濾
  • Hash Join 的優先順序高於 Nest Loop Join,因為 Hash join 本身是比 Nest Loop Join 快很多的。

Doris Join 調優方法

Doris Join 調優的方法:

  • 利用 Doris 本身提供的 Profile,去定位查詢的瓶頸。Profile 會記錄 Doris 整個查詢當中各種資訊,這是進行效能調優的一手資料。。
  • 瞭解 Doris 的 Join 機制,這也是第二部分跟大家分享的內容。知其然知其所以然、瞭解它的機制,才能分析它為什麼比較慢。
  • 利用 Session 變數去改變 Join 的一些行為,從而實現 Join 的調優。
  • 檢視 Query Plan 去分析這個調優是否生效。

上面的 4 步基本上完成了一個標準的 Join 調優流程,接著就是實際去查詢驗證它,看看效果到底怎麼樣。

如果前面 4 種方式串聯起來之後,還是不奏效。這時候可能就需要去做 Join 語句的改寫,或者是資料分佈的調整、需要重新去 Recheck 整個資料分佈是否合理,包括查詢 Join 語句,可能需要做一些手動的調整。當然這種方式是心智成本是比較高的,也就是說要在嘗試前面方式不奏效的情況下,才需要去做進一步的分析。

調優案例實戰

案例一

一個四張表 Join 的查詢,通過 Profile 的時候發現第二個 Join 耗時很高,耗時 14 秒。

進一步分析 Profile 之後,發現 BuildRows,就是右表的資料量是大概 2500 萬。而 ProbeRows ( ProbeRows 是左表的資料量)只有 1 萬多。這種場景下右表是遠遠大於左表,這顯然是個不合理的情況。這顯然說明 Join 的順序出現了一些問題。這時候嘗試改變 Session 變數,開啟 Join Reorder。

set enable_cost_based_join_reorder = true

這次耗時從 14 秒降到了 4 秒,效能提升了 3 倍多。

此時再 Check Profile 的時候,左右表的順序已經調整正確,即右表是大表,左表是小表。基於小表去構建雜湊表,開銷是很小的,這就是典型的一個利用 Join Reorder 去提升 Join 效能的一個場景

案例二

存在一個慢查詢,檢視 Profile 之後,整個 Join 節點耗時大概44秒。它的右表有 1000 萬,左表有 6000 萬,最終返回的結果也只有 6000 萬。

這裡可以大致的估算出過濾率是很高的,那為什麼 Runtime Filter 沒有生效呢?通過 Query Plan 去檢視它,發現它只開啟了 IN 的 Runtime Filter。

當右表超過1024行的話, IN 是不生效的,所以根本起不到什麼過濾的效果,所以嘗試調整 RuntimeFilter 的型別。

這裡改為了 BloomFilter,左表的 6000 萬條資料過濾了 5900 萬條。基本上 99% 的資料都被過濾掉了,這個效果是很顯著的。查詢也從原來的 44 秒降到了 13 秒,效能提升了大概也是三倍多。

案例三

下面是一個比較極端的 Case,通過一些環境變數調優也沒有辦法解決,因為它涉及到 SQL Rewrite,所以這裡列出來了原始的 SQL 。

select 100.00 * sum (case
        when P_type like 'PROMOS'
        then 1 extendedprice * (1 - 1 discount)
        else 0
        end ) / sum(1 extendedprice * (1 - 1 discount)) as promo revenue
from lineitem, part
where
    1_partkey = p_partkey
    and 1_shipdate >= date '1997-06-01'
    and 1 shipdate < date '1997-06-01' + interval '1' month

這個 Join 查詢是很簡單的,單純的一個左右表的 Join 。當然它上面有一些過濾條件,開啟 Profile 的時候,發現整個查詢 Hash Join 執行了三分多鐘,它是一個 BroadCast 的 Join,它的右表有 2 億條,左表只有 70 萬。在這種情況下選擇了 Broadcast Join 是不合理的,這相當於要把 2 億條做一個 Hash Table,然後用 70 萬條遍歷兩億條的 Hash Table ,這顯然是不合理的。

為什麼會產生不合理的 Join 順序呢?其實這個左表是一個 10 億條級別的大表,它上面加了兩個過濾條件,加完這兩個過濾條件之後, 10 億條的資料就剩 70 萬條了。但 Doris 目前沒有一個好的統計資訊收集的框架,所以它不知道這個過濾條件的過濾率到底怎麼樣。所以這個 Join 順序安排的時候,就選擇了錯誤的 Join 的左右表順序,導致它的效能是極其低下的。

下圖是改寫完成之後的一個 SQL 語句,在 Join 後面新增了一個Join Hint,在Join 後面加一個方括號,然後把需要的 Join 方式寫入。這裡選擇了 Shuffle Join,可以看到右邊它實際查詢計劃裡面看到這個資料確實是做了 Partition ,原先 3 分鐘的耗時通過這樣的改寫完之後只剩下 7 秒,效能提升明顯

Doris Join 調優建議

最後我們總結 Doris Join 優化調優的四點建議:

  • 第一點:在做 Join 的時候,要儘量選擇同型別或者簡單型別的列,同型別的話就減少它的資料 Cast,簡單型別本身 Join 計算就很快。
  • 第二點:儘量選擇 Key 列進行 Join, 原因前面在 Runtime Filter 的時候也介紹了,Key 列在延遲物化上能起到一個比較好的效果。
  • 第三點:大表之間的 Join ,儘量讓它 Co-location ,因為大表之間的網路開銷是很大的,如果需要去做 Shuffle 的話,代價是很高的。
  • 第四點:合理的使用 Runtime Filter,它在 Join 過濾率高的場景下效果是非常顯著的。但是它並不是萬靈藥,而是有一定副作用的,所以需要根據具體的 SQL 的粒度做開關。
  • 最後:要涉及到多表 Join 的時候,需要去判斷 Join 的合理性。儘量保證左表為大表,右表為小表,然後 Hash Join 會優於 Nest Loop Join。必要的時可以通過 SQL Rewrite,利用 Hint 去調整 Join 的順序。

以上就是Doris Join 優化原理檔案詳解的詳細內容,更多關於Doris Join 優化原理的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com