本文我們看下一條使用 Calcite 如何執行一條 SQL, 計劃用分三部分 1. SQL to Planner 2. Find Best Rel 3. Execution,來進行學習(恩 邊看邊學邊寫的我- -)。 本文專註於 SQL to Planner, 看下一條 SQL 文本在 Calcite 中如何轉換並進入 Plan 階段。

JDBC & Avatica

Calcite 作為一個框架,對外作為 Provider 實現了 Avactica 的 SPI,而 Avactica 則實現了 JDBC 協議, 支持通過 JDBC API 本地執行或發送到遠程 Server 執行 Avacita SPI Provider(這裡就是 calcite), Avacita 更多說明可以參考官方文檔~

所以我們要在 Calcite 中執行一條 SQL 最簡單的就是調用 JDBC 的 API(Example 中我們使用 sqlline 是一個調用 JDBC API 的 console), 我們看簡單使用例子(非 Prepare):

Connection connection = null;
Statement statement = null;
try {
Properties info = new Properties();
info.put("model", jsonPath(model));
connection = DriverManager.getConnection("jdbc:calcite:", info);
statement = connection.createStatement();
final ResultSet resultSet = statement.executeQuery(sql);
//...
} finally {
close(connection, statement);
}

就是很普通的 JDBC 使用, 簡單過下 Avactica 切入的地方

  • 首先獲取到的 connection 是 AvaticaConnection 類型的 conneciton
  • createStatement獲取的是特殊的 AvaticaStatement
  • executeQuery 就會地調用 calicate 的 prepareAndExecute 方法

prepareAndExecute 真正進入 calcite 的正式 SQL 處理流程, 對於 Avatica 就簡單介紹到這裡,總體是 JDBC 開發新 driver 的過程, 首先通過類載入註冊 driver,並通過 jdbc url 觸發 自己的 driver 獲取自己的 connection,然後實現 statement, result 等等, 他在實現過程中有通過 Factory 和 Meta 等暴露 SPI 給類似 Calcite 這樣的 Provider 實現... 本文我們還是重點關注 calcite 哈。

Schema 信息

查詢的前提是已經知道 schema 信息,在使用 Calcite 時, 我們可以通過 json 或 yml 的方式向其提供 schema 定義信息, 在初始化連接後 Avatica 會調用 Calcite 實現的 onConnectionInit SPI, 之後會根據 url 鏈接中的 model屬性從文件或 inline 讀取 schema 信息(JsonRoot), 在進一步根據其中的 schema 定義從 jdbc, map, custom 創建具體的 schema 並通過 visitor + stack 的方式遍歷合併所有 schema 作為 subSchema 加入到到 connection 的 `rootSchema` 中。

接著上面上節的 prepareAndExecute 往下會初始化 CalciteCatalogReader 來提供 table metadata 並做一些校驗後給後續 planner 使用, 所以後續 planner 只會和 CatalogReader 打交道。

選擇 Planner

前面幾篇文章中我們有提到過 Calcite 有多個 planner 實現, 在 CalcitePrepareImpl#prepare_ 中會調用 createPlannerFactories 目前這個方法實現會創建一個 VolcanoPlanner 並根據配置初始加入一些 Trait 和 rule, 其實這個方法的本意是可以創建多個 planner factory 列表, 然後可以先用簡單快速的 planner 處理,如果不行再用較重的 planner 再走一遍(drill 很像), 如果有需要我們可以選擇 override 這個方法。

SQL to SqlNode

接下來跟下去主要處理邏輯就在 prepare2_ 中。這個方法第一個參數是 Query 類型, 所以我們除了用 SQL 文本讓 Calcite 運行以外, 也可以選擇直接拼裝 Rel(也可以是 rpc 反序列化)執行,或使用 linq2j 構建 Queryable 表達式 執行。

不過相對"常用"的還是 SQL 文本的方式, 所以這裡主要專註SQL 文本的處理。 發現 query 中有 sql 字元串後, 會根據配置創建 parser 並將 sql 轉換為 SqlNode。

Calcite 的 parser 使用 javacc + freemarker 來靈活生成 parser, 可以看下 parser.jj 和 config.fmpp, parser 本身做源碼分析比較枯燥,這裡大家有興趣可以自己看下, 通過 parser 後我們獲取到 AST 也就是 SqlNode.

PS: 我們注意到 calcite 在 AST 出來後如果發現是 DDL 就直接用 SqlNode 處理,另外在非常靠前還沒 parser 和構建 catalogReader 就短路處理簡單如 select 1的 SQL(主要連接池探活用)。

SqlNode to RelRoot

之後我們看下 prepareSql, 暫時忽略 explain 等無關邏輯, 前半部分主要構造了 SqlToRelConverter , 然後 convertQuery 轉換 sqlNode 到 Rel 關係表達式。

SqlNode 是抽象語法樹(Abstract Syntax Tree)節點代表 SQL 代碼中一個節點, 而 Rel 代表關係表達式(Relation Expression), 所以從 AST 轉換為 Rel 的過程中即是一個轉換有是一個校驗關聯的過程,對應於其他資料庫優化器大概就是 Bind 或者 Preprocess 的過程

  1. Validate

在 convertQuery 中首先會用 SqlValidator 做一次 validate, 主要邏輯跟進後在 validateScopedExpression 中:

  • 首先會對遞歸 AST 樹做一些無條件的變換來簡化後續處理, 比如對 values 1 轉換為 select 1(其實這個代碼沒生效); 為 update, delete 生成對應的 select; 將 orderByNode併到 select 等, 代碼可以看下 performUnconditionalRewrites
  • 之後對處理後的 AST 遞歸調用 registerQuery, 根據不同的 Node Kind 生成生成 SqlValidatorNamespace 和 SqlValidatorScope, 作為映射維護到 Validator 中(IdentityMapping),整個 register 過程的目標是對不同的解析 resolve scope 生成不同的 scope 和 namespace, 這個過程中也會對 AST 做一些改寫(e.g. from tbl => from tbl as tbl)
  • 之後會對 AST 調用 validate, 校驗過程會依賴於上一步的 scope 和 validator 中映射信息對 AST 進行校驗; 在校驗過程中還完成不少事情: 做類型推斷(DeriveTypeVisitor); 對表達式和 star 做展開 等

這部分代碼挺複雜,總結下就是在第2步中整體遍歷整理出 scope 和 namespace(個人目前理解: scope 用於限制可見作用域, namespace用於整合多個符號)在第3步中根據上一步的信息驗證並將節點加上語義信息(select a from t1, t2, 知道 a 是操作的 t1),並且完成類型推斷(select abs(a) from tblabs(a) 是 int 還是 float)。

2. Convert to RelNode

在獲取了合法且補充了"一定"語義信息的 AST 樹後, 且 validator 中已經準備好各種 scope/namespace 的映射信息後就開始具體到 RelNode 的轉換, 會根據不同類型的語句進行處理, 這裡用 select 為例看下~

  • 這個轉換過程中會使用多個 Blackboard 作為轉換上下文並附著一些幫助方法
  • 首先會轉換 from 部分,用最簡單的例子看就是一個 table, 首先會找到上一步中的 tableNameSpace, 並用名字從前準備好的 catalogReader 中獲取 table schema 信息, 之後根據 schema 創建 RelNode(e.g. 比如普通的 LogicalTableScan並根據虛列信息決定是否添加 projection; 如果是 view 那會轉換做一個 view 展開邏輯), 這一步還會根據 isConvertTableAccess 決定是是否對 Table 轉換為物理 Table(e.g. TranslatableTable)
  • 之後假設有 where, 則進行 where 部分轉換, 首先會將表達式轉換為 RexNode 然後創建 LogicalFilter 的 RelNode(這個 create 過程會查找 RelMetadataQuery 獲取 traitSet 信息), 並將之前的 LogicalTableScan 作為 input 而自己提換充當 blackboard 的新 root
  • 後面對 order, agg, projection 的處理很類似還是直接看代碼吧, 逃~
  • PS: 在這個轉換過程中 Calcite 會做一些明顯且簡單的"優化"(e.g. 都是 true 就不加 filter, 對 rexNode 也會用 RexSimplify 做簡化)

最後在 BlackBoard 的 root 上就是我們希望獲取的 RelNode 樹, 最後在進行一些輸出類型校驗操作後, 和 collation 和 類型信息一起組裝成 RelRoot 供後面使用。

3. Decorrelate & Trim Field

之後會對剛才回去的 RelNode 樹結構做 decorrelate 並 trim 掉無用的欄位, 本文過長打算後開坑再看再寫~~

等待 Optimize

到這裡,我們已經將一個 sql 文本轉換為一個相貌平平的關係表達式(RelNode)樹了,並且已經準備好的一個 planner, 接下來就是把這顆樹塞到 Planner 進行 Optimize 獲取一個"更優"的樹的過程, 接下的部分正是大家最喜歡看 Calcite 的部分,下期再見~

PS: 本文介紹的校驗和轉換 Rel 部分邏輯細節很多其實感覺我自己可以花更多時間看看 - -


推薦閱讀:
相关文章