ByteHouse 查詢最佳化器的設計與實現

danny_2018發表於2023-12-21

作為企業級資料庫的核心元件之一,查詢最佳化器的地位不可忽視。對於眾多依賴資料分析的現代企業來說,一個強大且完善的查詢最佳化器能夠為資料管理和分析工作帶來巨大的便利。

作為一款火山引擎推出的雲原生資料倉儲,ByteHouse基於開源ClickHouse構建,並在位元組跳動內外部場景的檢驗下,對OLAP引擎能力、效能、運維、架構進一步升級。ClickHouse以快速處理資料而著名,但其查詢最佳化器在處理多表查詢和高維度資料時卻顯得力不從心。為了解決這一問題,火山引擎ByteHouse自研並推出了一款全新的查詢最佳化器。

本篇文章來源於火山引擎ByteHouse技術專家《ByteHouse查詢最佳化器的設計與實現》的分享,從現狀分析、設計思路、實現方案、高階最佳化、最佳化效果五個部分,拆解ByteHouse查詢最佳化器如何實現效能10倍提升。

現狀分析

ClickHouse 的儲存引擎、向量化計算擁有獨特的優勢。

ClickHouse缺乏複雜查詢的最佳化以及執行能力,比如說多表 JOIN 的效能、子查詢的執行,有很多的複雜的查詢在 ClickHouse 上是無法執行的或者執行效能比較差。

社群在嘗試構建 query plan 的概念和最佳化器相關的模組,但是現還處於比較初級的階段。而且 ClickHouse 的下發執行查詢的方式比較奇特:收到 SQL 的主 Server 會做一定的解析,然後會將解析完的結果又重新生成一個 SQL 再發到其他的 worker 上。這種方法能表達的資訊是有限的,SQL 能表達什麼樣的資訊,它其實就只能下發這樣這些資訊給其他的 Server 去執行。

syntax analyze和 tree writer 相關的模組複雜,有很多歷史遺留的問題,設計上比較繁瑣,所以在支援一些比較複雜的查詢的時候它有一定的侷限性。

設計思路

基於以上原因,ByteHouse團隊重新實現了ClickHouse 的最佳化器,左邊的是ByteHouse的查詢最佳化器的資料流相關的部分。

首先客戶端傳來一條 SQL字串,server 會將 SQL 字串解析成AST,再將 AST 經過一系列的分析過程以及構建的過程構建出一個 query plan, query plan 經過一定的最佳化生成最好的計劃,將這一個分散式計劃切成 plansegment 去下發執行。

右邊的紫色部分就是從 0 到 1 全部重新實現的一個最佳化器相關的模組。SQL 字串經過Parser之後,然後再經過自研的最佳化器,最終會下發一個 query plan 到 MPP 的執行引擎上進行執行,最終將結果返回給client,除了 Parser 以外到執行之前的其他模組全部進行了重寫,這樣就會保證語法上和開源 ClickHouse 做了相容,但是中間的分析和最佳化部分全部都是自研的,而且對於下發查詢的方式也做了一定的改變,並不是轉化成一個 SQL 下發到其他 Server 上執行,而是下發一個完整的 query plan 到不同的機器上,然後用 MPP 的方式進行執行。

主要模組:

第一個模組是Analyzer,主要分兩部分,一個是 query writer,一個是 query analyzer。query writer 是在 AST 級別對查詢進行一定的改寫,比如說 with CTE/ view/ UDF 的這些簡單的內容的展開,包括一些特殊函式的替換,比如使用者雖然寫了一個函式 count distinct 某一列,但是其實我們最終會轉化成另外的一個函式來進行執行。這種簡單的替換,是在 AST 級別來做的。第二部分是 query analyzer,主要是對名字進行解析,對資料型別和語法的一些校驗,最終將整個分析的結果抽象化成一個結構化的資料結構,用來輔助後面的 query plan 的構建。整個資料結構能描述查詢他想要表達的語義,然後利用這個結構化的資料結構構建出 query plan。

第二個模組是plan builder:改進社群 QueryStep 內容;增加序列化反序列化;補充高階運算元。

第三個模組是optimizer:RBO,CBO,分散式計劃最佳化,高階最佳化能力(Runtime Filter,CTE,物化檢視改寫)

第四個模組是Statistics:以 Histogram 為主的統計資訊;自動收集和更新

第五個模組是Diagnosis Tools:Plan Explain,Explain Analyze,Plan Visualization,Plan Dump

實現方案

最佳化器:經過一些規則的最佳化,把一個計劃變成另外一個更好的計劃。

Role Based Optimizer:根據最佳化規則對關係表示式進行轉換,一個計劃經過最佳化規則後會變成另外一個計劃,同時原有計劃會被裁剪掉,經過一系列轉換後生成最終的執行計劃。

Cost Based Optimizer:透過規則生成一系列計劃,利用統計資訊評估計劃的代價,選擇代價最低的作為最終計劃。

除最佳化框架之外,還需要很多最佳化理論來應用這兩個框架對計劃進行變化。

主要有這四種能力:

基於關係代數的等價性 :join 交換律於結合率

基於資料特性:唯一鍵,functional dependency

基於分散式資料庫特性:exchange 插入,運算元拆分

高階最佳化手段:物化檢視,Runtime Filter

RBO

RBO主要實現了兩種最佳化改寫框架:

基於 visitor 的改寫框架

Top-Down / Botton-Up 的方式對一個 QueryPlan 做改寫,適合於對上下文有依賴的最佳化規則。比如說把一個predicate 不停向下傳遞,將每一個條件放到它最應該存在的地方,讓它儘量早地執行。這需要從上到下全部鏈路進行改寫並傳遞資訊進行最佳化。例如:predicate push down 和 Column pruning

基於Pattern-match

適合簡單、通用的改寫規則。例如:將兩個連續的 filter 節點合併成一個,這樣關注連續的兩個是 filter ,然後把這兩個進行合併,再替換回去完成最佳化了,並不需要對整個查詢改寫。

利用這兩種改寫框架實現了常見的最佳化規則,比如列裁剪,表示式的簡化以及子查詢的結關聯,謂詞下推,還包括冗餘運算元消除,Outer Join 轉 Inner Join,運算元下推儲存、分散式運算元拆分等常見的啟發式最佳化能力。

RBO 解關聯

左邊:這樣一個SQL看起來只是查一張表,但其過濾條件裡面又用了另一張表。子查詢中既用了自己的列,又用到外面主查詢的列。子查詢和主查詢是有一定的互相依賴的關聯查詢,這樣的查詢其實是正常情況下是很多資料庫是不能直接執行的。

右邊:經過解關聯之後的一個查詢計劃的樣子,轉換成了常見的運算元 join agg 等,這樣查詢就可以正常執行了。

CBO

藍色的 query plan經過右邊的 optimize task 不停地擴充套件,利用右邊的黃色的 rule(比如 join reorder 和CTE)轉化,中間的搜尋空間會被擴充,然後計算 cost,最終會基於 cost 選擇一個最好的 plan 作為最終輸出結果。而且在最佳化的過程中,因為是一個分佈資料庫,所以會利用一些 property 的資訊去生成分散式計劃,同樣也會將分散式計劃的 cost 考慮在內並基於綜合代價去選擇一個最優解。

詳細展開一下 CBO 裡內的每一個模組審具體是怎樣實現。

CBO Cascades搜尋框架:

下邊這個表格描述了在不同 join 表的數量的情況下它真正要表達的搜尋空間是多大。其實是階乘級別的複雜度,對於10 個表來說已經是億級別的量級了,由於列舉數量龐大的邏輯執行計劃是不現實的,儲存空間和搜尋時間是接受不了的,所以就利用了 Cascades 的搜尋框架,Group 和 GroupExpr 來表達數量龐大的搜尋空間,可以將 n 的階乘的複雜度的搜尋空間來降低到 3 的 n 次方級別。這樣可以在有限的時間內搜尋出 10 表以內的所有的 join 情況,可以大大地降低整個搜尋時間以及儲存空間。

CBO計劃列舉

規則

需要靠CBO的規則不停地擴充搜尋空間,然後基於cost選出其中的最優解,擴充得越多,選出最優解可能性就更高。我們也實現了常見的 CBO rule。例如:Inner Join Reorder,Left Join to Right Join,Pull Left through Inner以及CTE和Magic Set這樣高階最佳化。

分散式計劃

作為分散式資料庫,ByteHouse會利用分散式的屬性,將分散式計劃的生成和搜尋融合在同一個Cascades搜尋框架內,最終基於代價來選擇最優的分散式計劃。

利用三種property來最佳化和生成分散式計劃:

partitioning 是指資料是如何分割槽的,每個資料分割槽內的資料是不相交的。例如第一個分割槽內的資料是AB,那第二個分割槽內資料是CD,所有 A 或者 B 都會在第一個分割槽內,所有 C 和 D 都會在第二個分割槽內。

grouping 和 sorting 描述的是資料行與行之間的關係。grouping 是相同值的資料都要連續地排布在一起,例如 BBA。sorting 被認為是一對 grouping 的加強,它不但要連續的資料在一起,而且是要有序的,例如 AABB。

我們將分散式計劃和 CBO 框架結合在了一起。很多資料庫的做法是用 CBO 先生成一個計劃,在這個計劃上推導如何插入 exchange 的節點,這樣有可能會 miss 掉最優解,很多情況下可能是一個次優的 join order ,但加上生成的分散式計劃,它反而是全域性最優的。

以a、b, c 三個表為例子,這兩個橙色計劃分別對應兩個不同的單機計劃(對於每一個單地計劃,其實都可以擴容出多個分散式計劃)。比如說我們先進行 a join b,再 join c,這樣的情況 a 表按 uid 進行repatition,這樣 a b 都按 uid 進行分割槽之後,可以直接進行 join ,然後對 join 的結果再按 id 進行repatition,然後去和 c 進行join,這樣的結果才是正確的。但也可以不需要對a做repatition,也可以對b做replicated,這就有兩種不同的選擇。對於另一個單機計劃來說,同樣也可以生成兩種不同的資料分佈方式。會對這四個計劃都會去評估代價是多少,哪一個花費的時間更少,那基於 cost 選擇最終的最優解。

高階最佳化

Runtime Fliter

此查詢是對 sales 表和 item 表進行 join。現對 item ID 做了一個大於 1000 的過濾,利用過濾之後的 id 構建 filter,對 sales 用這個 filter 來過濾資料,過濾之後會讓真正參與 join 的資料減少很不多。相比於正常執行,這種最佳化大大減少左邊表格的掃描量,效率更高。

但是至於是否做這樣的最佳化是需要做一定決策的,並不是所有 join 都能做這樣的最佳化,而且最佳化是有一定代價的。決策和最佳化都需要最佳化器去決策。

CTE (common table expression):

這個查詢有三種可能性, SQL 是對同一張表 join 了三次,但是有不同的過濾條件, V1、V2 都是對這張表的 current price 做了一個小於 1000 的一個過濾之後進行join,那 V3在過濾之後還做了一個 color = red 的過濾。

第一個計劃只做了一遍 tablescan,把掃描結果用了三次,但沒法利用到color這個列已經建了索引的能力。掃描的時候要把整個表都掃上來,然後在記憶體裡做price 小於 1000 的過濾。

第二個計劃前兩張表 V1 和 V2 是不要進行共享,V3 也不要進行共享,但是 V3 這個這個表在掃描的時候可以利用 index scan 的能力,因為在 color的這一列上建了索引,所以就用 index scan 的能力掃描資料,V3的掃描代價比較低。這些計劃都不是最好的,應該讓前面兩個做共享,然後第三個自己獨立的掃描,這才是最優的一個計劃。

至於哪一個更好,要利用 Cascades 全域性地去搜尋,列舉這些計劃,基於 cost 判斷。

最佳化效果

在沒有最佳化器的時,僅能完成 26 個SQL的查詢,而且是在對一些 SQL 做了一定變換之後才能執行的。

新增最佳化器之後,ByteHouse能夠正常跑完 TPC-DS的全部99 條查詢,而且它的效能也是比較不錯的。在之前同樣都能支援的26 個查詢中,它的效能也得到了極大的提升。

來自 “ 位元組跳動技術團隊 ”, 原文作者:景鵬;原文連結:https://mp.weixin.qq.com/s/7LhG230SjK5MwrvWmxHfvQ,如有侵權,請聯絡管理員刪除。

相關文章