前言
為了支援複雜的 SQL 查詢,並且提供更好的使用體驗,我們在最近的幾個月裡對 Databend 的 SQL planner 進行了大規模的重構。目前重構已經接近尾聲,感興趣的朋友可以通過修改 Databend 的 Session settings
SET enable_planner_v2 = 1
來啟用新 planner 進行搶先體驗。
功能亮點
更加友好的查詢體驗
無論是資料分析師還是開發人員,在編寫 SQL 查詢的時候總會遇到各種各樣的報錯。尤其是在 SQL 查詢較為複雜的情況下,排查報錯成了許多人的噩夢(筆者本人曾經修改過有數十個 JOIN 子句的 MySQL 查詢,從此對 MySQL 的錯誤提示深惡痛絕)。
為了改善這方面的使用者體驗,我們在新的 Planner 中引入了嚴格的語義檢查環節,使得大部分的錯誤可以在查詢編譯階段就被攔截。同時為了方便使用者定位錯誤的位置,我們也引入了全新的錯誤提示演算法。
當你的 SQL 查詢使用了錯誤的語法時(比如寫錯了關鍵字,或者遺漏了某些子句),Databend 會為你提供提示資訊:
當你的 SQL 查詢出現語義上的錯誤時(比如使用了不存在的 Column,或者 Column 具有歧義),Databend 也會為你指出錯誤出現的位置:
在編寫複雜查詢時,依然可以獲得較好的體驗:
支援 JOIN 查詢與關聯子查詢
在新的 SQL planner 中,我們支援了 JOIN 查詢(INNER JOIN,OUTER JOIN,CROSS JOIN)與關聯子查詢,並且提供了 Hash Join 演算法用以執行 JOIN 查詢。
JOIN 查詢的相關文件已經發布在 https://databend.rs/doc/refer...,你可以查閱文件以瞭解 Databend 中 JOIN 查詢的使用方式。
在 OLAP 查詢中,JOIN 是非常重要的一部分。在傳統的星型模型和雪花模型中,我們都需要通過 JOIN 查詢將維度表與事實表連線起來以生成結果報表。
TPCH Benchmark 是由 TPC 委員會制定的一套 OLAP 查詢基準測試標準,用於評測資料庫系統的 OLAP 能力。其中包含了 8 張表,分別是:
- Lineitem:產品專案
- Orders:訂單資訊
- Customer:顧客資訊
- Part:零部件資訊
- Supplier:供應商資訊
- Partsupp:零件與供應商的關係表
- Nation:國家資訊
- Region:地區資訊
TPCH 中有 22 條複雜的查詢,對應不同的商業需求。這裡以 Q9 查詢為例,它的用途是計算指定年度和地區的利潤額,其中包含了大量的 JOIN 計算。在新的 Planner 中,我們已經可以支援該查詢:
關聯子查詢同樣也是 SQL 中的重要組成部分,通過關聯子查詢可以輕鬆表示複雜的查詢邏輯。TPCH 的 Q4 就是一個例子,它的用途是計算一段時間內各優先順序的訂單的交付情況。其中使用了 EXISTS 關聯子查詢來篩選逾期收貨的訂單:
目前 Databend 僅支援了關聯子查詢的簡單執行,相關的查詢優化工作仍在進行中,敬請期待。
全新架構
新的 SQL planner 中我們對 SQL 解析的流程進行了重新設計,以支撐更加複雜的語義分析和 SQL 優化。在新的 SQL planner 中,一條 SQL 語句通過客戶端傳送到 databend-query server 後,會按照下圖所示的順序由不同的元件進行處理,最終將查詢的結果返回給客戶端:
收到 SQL 查詢後,Parser 元件會對其進行解析。在此步驟中如果遇到了語法錯誤則會直接將錯誤資訊返回給客戶端,解析成功則會生成查詢對應的 AST(抽象語法樹)。
Parser
為了提供更豐富的語法分析功能和更好的開發體驗,我們開發了一套基於 nom Parser combinator 的 DSL (領域特定語言) nom-rule,並基於該框架重新編寫了 SQL Parser。
在這套框架下我們可以非常輕鬆地定義一條 Statement 的語法,以 CREATE TABLE 語句為例,我們可以使用 DSL 將其簡單描述為:
CREATE ~ TABLE ~ #identifier ~ "(" ~ (#column_def)+ ~ ")" ~ ";"
優雅的語法大大提高了編寫 Parser 的樂趣,歡迎有興趣的朋友們進行嘗試。
Binder
由 Parser 成功解析出 AST 後,我們會通過 Binder 對其進行語義分析,並且生成一個初始的 Logical Plan(邏輯計劃)。在此過程中,我們會進行不同型別的語義分析:
- Name resolution:通過查詢 Databend Catalog 中相關的 Table, Column 物件資訊,來檢查 SQL 查詢中引用的變數的合法性。並將合法的變數與對應的物件進行繫結,以進行後續的分析。
- Type check:根據 name resolution 中拿到的資訊,對錶達式的合法性進行檢查,並且為表示式尋找合適的返回型別。
- Subquery unnesting:將表示式中的子查詢提取出來,翻譯成關係代數的形式
- Grouping check: 對於含有聚合計算的查詢,分析是否在聚合函式以外引用了非聚合列
通過語義分析,我們可以排除掉絕大多數的語義錯誤,並在編譯階段將其返回給使用者,以提供最佳的錯誤排查體驗。
Optimizer
得到初始的 Logical Plan 後,優化器會對其進行改寫和優化,最終生成一個可執行的 Physical Plan 。
在新的 Planner 中,我們引入了一套基於 Transformation Rule 的優化器框架(Volcano/Cascades)。通過定義一個關係代數子樹結構的 Pattern 以及相關的 Transform 邏輯,即可實現一個獨立的 Rule。
以簡單的 Predicate Push Down 為例:
我們只需要定義輸入的 Plan 的 Pattern:
impl RulePushDownFilterProject {
pub fn new() -> Self {
Self {
id: RuleID::PushDownFilterProject,
// Filter
// \
// Project
// \
// *
pattern: SExpr::create_unary(
Pattern {
plan_type: RelOp::Filter,
},
SExpr::create_unary(
Pattern {
plan_type: RelOp::Project,
},
SExpr::create_leaf(
Pattern {
plan_type: RelOp::Pattern,
},
),
),
),
}
}
}
並且實現一個進行轉換的函式:
impl RulePushDownFilterProject {
pub fn apply(&self, s_expr: SExpr) -> Result<SExpr> {
let filter = s_expr.plan().into();
let project = s_expr.child(0).plan().into();
let result = SExpr::create_unary(
project,
SExpr::create_unary(
filter,
s_expr.child(0).child(0)
)
);
Ok(result)
}
}
Interpreter
通過 Optimizer 生成 Physical Plan 後,我們會將其翻譯成可執行的 Pipeline,並交由 Databend 的 Processor 執行框架進行計算。至此 Planner 的工作就告一段落,相信讀者也對新 Planner 的架構有了一個初步的瞭解。更多的技術細節請關注我們的後續文章。
未來規劃
從頭構建一個 SQL Planner 是一件十分具有挑戰性的事情,但是通過重新的設計和開發,我們可以找到最適合系統本身的架構與功能。在未來的一段時間裡,我們將持續完善和鞏固新的 SQL Planner,功能方面則會注重於:
- Cost-based Optimization(CBO, 基於代價的優化)
- 分散式查詢優化
- 更多的優化規則
目前,新的 SQL planner 的遷移工作已經接近尾聲,你可以通過該 issue 追蹤進度。預計在七月份內所有的遷移工作將會完成, 屆時我們將會發布版本更新的公告,敬請期待。
關於 Databend
Databend 是一款開源、彈性、低成本,基於物件儲存也可以做實時分析的新式數倉。期待您的關注,一起探索雲原生數倉解決方案,打造新一代開源 Data Cloud。
- Databend 文件:https://databend.rs/
- Twitter:https://twitter.com/Datafuse_...
- Slack:https://datafusecloud.slack.com/
- Wechat:Databend
- GitHub :https://github.com/datafusela...
文章首發於公眾號:Databend