1.簡介和架構
Apache Calcite 是一個動態的數據管理框架, 可以實現 SQL 的解析、驗證、優化和執行。Calcite 是模塊化和插件式的, 解析、驗證、優化和執行的步驟都對應著一個相對獨立的模塊。用戶可以選擇使用其中的一個或多個模塊,也可以對任意模型進行定制化擴展。
Calcite 的架構如下圖所示,
① JDBC:構建了一個獨立的 Avatica 框架,可以通過標準的 JDBC 接口訪問 Calcite 獲取數據。
② SQL Parser 和 SQL Validator:可以進行 SQL 的解析和驗證,,并將原始的 SQL 字符串解析并轉化為內部的 SqlNode
樹來表示。
③ Query Optimizer:進行查詢優化,,基于在關系代數在 Calcite 內部有一種關系代數表示方法,將關系代數表示為 RelNode
樹。RelNode
樹不只是由 SqlNode
樹轉化而來,也可以通過Calcite 提供的 Expressions Builder 接口構建。
說明:Calcite 包含許多組成典型數據庫管理系統的部件,但是省略了一些關鍵的組成部分,例如數據的存儲、處理數據的算法和存儲元數據的存儲庫等。因為對不同的數據類型有不同的存儲和計算引擎,是不可能將它們統一到一個框架的,所以 Calcite 是一個統一的 SQL 接口實現數據訪問框架。
2.SQL 處理流程
如下圖所示,Calcite 的處理流程實際上就是 SQL 的解析、校驗、優化和執行。
① Parser:解析 SQL,將輸入的 SQL 字符串轉化為抽象語法樹(AST),即 SqlNode
樹表示
② Validator:根據元數據信息對 SqlNode
樹進行驗證, 其輸出仍是 SqlNode
樹
③ Converter:將 SqlNode
樹轉化為關系代數,其中 RelNode
樹表示關系代數
④ Optimizer:對輸入的關系代數進行優化,并輸出優化后的 RelNode
樹
⑤ Execute:根據優化后的 RelNode
生成執行計劃
3.案例分析
users 表的內容:
id:string,name:string,age:int
1,Jack,28
2,John,21
3,Tom,32
4,Peter,24
orders 表內容:
id:string,user_id:string,goods:string,price:double
001,1,Cola,3.5
002,1,Hat,38.9
003,2,Shoes,199.9
004,3,Book,39.9
005,4,Phone,2499.9
查詢的 SQL 語句:
SELECT u.id, name, age, sum(price)
FROM users AS u join orders AS o ON u.id = o.user_id
WHERE age >= 20 AND age <= 30
GROUP BY u.id, name, age
ORDER BY u.id
3.1 SQL 解析
通過詞法分析和語法分析將 SQL 字符串轉化為 AST。在Calcite中, 借助 JavaCC 實現了 SQL 的解析, 并轉化為 SqlNode
表示。
SqlNode
是 AST 的抽象基類,不同類型的節點有對應的實現類。下面的 SQL 語句會生成 SqlSelect
和 SqlOrderBy
兩個主要的節點。
String sql = "SELECT u.id, name, age, sum(price) " +
"FROM users AS u join orders AS o ON u.id = o.user_id " +
"WHERE age >= 20 AND age <= 30 " +
"GROUP BY u.id, name, age " +
"ORDER BY u.id";
// 創建SqlParser, 用于解析SQL字符串
SqlParser parser = SqlParser.create(sql, SqlParser.Config.DEFAULT);
// 解析SQL字符串, 生成SqlNode樹
SqlNode rootSqlNode = parser.parseStmt();
上述代碼中的 rootSqlNode
是 AST 的根節點。如下圖所示,可以看到 rootSqlNode
是SqlOrderBy
類型,其中 query
字段是一個 SqlSelect
類型,即代表原始的 SQL 語句去掉ORDER BY 部分。
3.2 SQL 校驗
SQL 校驗階段一方面會借助元數據信息執行上述驗證,另一方面會對 SqlNode
樹進行一些改寫,以轉化為統一的格式。
// 創建Schema, 一個Schema中包含多個表
SimpleTable userTable = SimpleTable.newBuilder("users")
.addField("id", SqlTypeName.VARCHAR)
.addField("name", SqlTypeName.VARCHAR)
.addField("age", SqlTypeName.INTEGER)
.withFilePath("/path/to/user.csv")
.withRowCount(10)
.build();
SimpleTable orderTable = SimpleTable.newBuilder("orders")
.addField("id", SqlTypeName.VARCHAR)
.addField("user_id", SqlTypeName.VARCHAR)
.addField("goods", SqlTypeName.VARCHAR)
.addField("price", SqlTypeName.DECIMAL)
.withFilePath("/path/to/order.csv")
.withRowCount(10)
.build();
SimpleSchema schema = SimpleSchema.newBuilder("s")
.addTable(userTable)
.addTable(orderTable)
.build();
CalciteSchema rootSchema = CalciteSchema.createRootSchema(false, false);
rootSchema.add(schema.getSchemaName(), schema);
RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl();
// 創建CatalogReader, 用于指示如何讀取Schema信息
Prepare.CatalogReader catalogReader = new CalciteCatalogReader(
rootSchema,
Collections.singletonList(schema.getSchemaName()),
typeFactory,
config);
// 創建SqlValidator, 用于執行SQL驗證
SqlValidator.Config validatorConfig = SqlValidator.Config.DEFAULT
.withLenientOperatorLookup(config.lenientOperatorLookup())
.withSqlConformance(config.conformance())
.withDefaultNullCollation(config.defaultNullCollation())
.withIdentifierExpansion(true);
SqlValidator validator = SqlValidatorUtil.newValidator(
SqlStdOperatorTable.instance(), catalogReader, typeFactory, validatorConfig);
// 執行SQL驗證
SqlNode validateSqlNode = validator.validate(node);
如下圖可知,SQL 驗證后的輸出結果仍是 SqlNode 樹。不過其內部結構發生了改變,一個明顯的變化是驗證后的 SqlOrderBy
節點被改寫為了 SqlSelect
節點,并在其 orderBy
變量中記錄了排序字段。
如果把表名或者字段寫錯,validator.validate(node)
運行時在就會報錯。如果把驗證前后的SqlNode
完全打印出來,可以發現在校驗時會為每個字段加上表名限定。
-- 驗證前的SqlNode樹打印結果
SELECT `u`.`id`, `name`, `age`, SUM(`price`)
FROM `users` AS `u`
INNER JOIN `orders` AS `o` ON `u`.`id` = `o`.`user_id`
WHERE `age` >= 20 AND `age` <= 30
GROUP BY `u`.`id`, `name`, `age`
ORDER BY `u`.`id`
-- 驗證后的SqlNode樹打印結果
SELECT `u`.`id`, `u`.`name`, `u`.`age`, SUM(`o`.`price`)
FROM `s`.`users` AS `u`
INNER JOIN `s`.`orders` AS `o` ON `u`.`id` = `o`.`user_id`
WHERE `u`.`age` >= 20 AND `u`.`age` <= 30
GROUP BY `u`.`id`, `u`.`name`, `u`.`age`
ORDER BY `u`.`id`
3.3 轉換為關系代數 RelNode
關系代數是 SQL 的理論基礎,可以閱讀 Introduction of Relational Algebra in DBMS簡單了解,其中“數據庫系統概念“中對關系代數有更深入的介紹。
在 Calcite 中, 關系代數由 RelNode
表示。如下代碼所示,將校驗后的 SqlNode
樹轉化為RelNode
樹。
// 創建VolcanoPlanner, VolcanoPlanner在后面的優化中還需要用到
VolcanoPlanner planner = new VolcanoPlanner(RelOptCostImpl.FACTORY, Contexts.of(config));
planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
// 創建SqlToRelConverter
RelOptCluster cluster = RelOptCluster.create(planner, new RexBuilder(typeFactory));
SqlToRelConverter.Config converterConfig = SqlToRelConverter.config()
.withTrimUnusedFields(true)
.withExpand(false);
SqlToRelConverter converter = new SqlToRelConverter(
null,
validator,
catalogReader,
cluster,
StandardConvertletTable.INSTANCE,
converterConfig);
// 將SqlNode樹轉化為RelNode樹
RelNode relNode = converter.convertQuery(validateSqlNode, false, true);
RelNode
樹實質上是一個邏輯執行計劃,上述 SQL 對應的邏輯執行計劃如下,其中每一行都表示一個節點,即 RelNode
的實現類。
LogicalSort(sort0=[$0], dir0=[ASC])
LogicalAggregate(group=[{0, 1, 2}], EXPR$3=[SUM($3)])
LogicalProject(id=[$0], name=[$1], age=[$2], price=[$6])
LogicalFilter(condition=[AND(>=($2, 20), <=($2, 30))])
LogicalJoin(condition=[=($0, $4)], joinType=[inner])
LogicalTableScan(table=[[s, users]])
LogicalTableScan(table=[[s, orders]])
3.4 查詢優化
查詢優化是 Calcite 的核心模塊,主要有三部分組成:
① Planner rules:優化規則,例如內置優化規則有謂詞下推、投影下推等。用戶也可定義自己的優化規則。
② Metadata providers:元數據,主要用于基于成本的優化(Cost-based Optimize 即 CBO),包括表的行數、表的大小、給定列的值是否唯一等信息。
③ Planner engines:優化器實現,HepPlanner
用于實現基于規則的優化(Rule-based Optimize 即 RBO),VolcanoPlanner
用于實現基于成本的優化。
// 優化規則
RuleSet rules = RuleSets.ofList(
CoreRules.FILTER_TO_CALC,
CoreRules.PROJECT_TO_CALC,
CoreRules.FILTER_CALC_MERGE,
CoreRules.PROJECT_CALC_MERGE,
CoreRules.FILTER_INTO_JOIN, // 過濾謂詞下推到Join之前
EnumerableRules.ENUMERABLE_TABLE_SCAN_RULE,
EnumerableRules.ENUMERABLE_PROJECT_TO_CALC_RULE,
EnumerableRules.ENUMERABLE_FILTER_TO_CALC_RULE,
EnumerableRules.ENUMERABLE_JOIN_RULE,
EnumerableRules.ENUMERABLE_SORT_RULE,
EnumerableRules.ENUMERABLE_CALC_RULE,
EnumerableRules.ENUMERABLE_AGGREGATE_RULE);
Program program = Programs.of(RuleSets.ofList(rules));
RelNode optimizerRelTree = program.run(
planner,
relNode,
relNode.getTraitSet().plus(EnumerableConvention.INSTANCE),
Collections.emptyList(),
Collections.emptyList());
經過優化后的輸出如下,可知所有的節點都變成了 Enumerable
開頭的物理節點,其基類是EnumerableRel
EnumerableSort(sort0=[$0], dir0=[ASC])
EnumerableAggregate(group=[{0, 1, 2}], EXPR$3=[SUM($3)])
EnumerableCalc(expr#0..6=[{inputs}], proj#0..2=[{exprs}], price=[$t6])
EnumerableHashJoin(condition=[=($0, $4)], joinType=[inner])
EnumerableCalc(expr#0..2=[{inputs}], expr#3=[Sarg[[20..30]]], expr#4=[SEARCH($t2, $t3)], proj#0..2=[{exprs}], $condition=[$t4])
EnumerableTableScan(table=[[s, users]])
EnumerableTableScan(table=[[s, orders]])
優化前后的計劃:users 表的過濾位置發生了變動,從先 Join
后過濾,變成了先過濾后 Join
,如下圖所示。
3.5 執行計劃
將物理計劃轉化為執行計劃通常需要自定義代碼。Calcite 提供了一種執行計劃生成方法,如下所示,可以生成執行計劃并讀取CSV文件中的數據。
EnumerableRel enumerable = (EnumerableRel) optimizerRelTree;
Map<String, Object> internalParameters = new LinkedHashMap<>();
EnumerableRel.Prefer prefer = EnumerableRel.Prefer.ARRAY;
Bindable bindable = EnumerableInterpretable.toBindable(internalParameters,
null, enumerable, prefer);
Enumerable bind = bindable.bind(new SimpleDataContext(rootSchema.plus()));
Enumerator enumerator = bind.enumerator();
while (enumerator.moveNext()) {
Object current = enumerator.current();
Object[] values = (Object[]) current;
StringBuilder sb = new StringBuilder();
for (Object v : values) {
sb.append(v).append(",");
}
sb.setLength(sb.length() - 1);
System.out.println(sb);
}
執行結果:
1,Jack,28,42.40
2,John,21,199.90
4,Peter,24,2499.90
參考:
[1] SQL over anything with an Optiq Adapter
[2] Apache Calcite 處理流程詳解(一)
[3] 編譯原理實踐 - JavaCC 解析表達式并生成抽象語法樹.