不想看文章直接訪問mysql-protocal(Java版本的Mysql)、calcite-test
,這里有關(guān)于Calcite RBO,CBO使用具體用例
1. 什么是Apache Calcite ?
Apache Calcite 是一款開源SQL解析工具, 可以將各種SQL語句解析成抽象語法術(shù)AST(Abstract Syntax Tree), 之后通過操作AST就可以把SQL中所要表達(dá)的算法與關(guān)系體現(xiàn)在具體代碼之中。
Calcite的生前為Optiq(也為Farrago), 為Java語言編寫, 通過十多年的發(fā)展, 在2013年成為Apache旗下頂級(jí)項(xiàng)目,并還在持續(xù)發(fā)展中, 該項(xiàng)目的創(chuàng)始人為Julian Hyde, 其擁有多年的SQL引擎開發(fā)經(jīng)驗(yàn), 目前在Hortonworks工作, 主要負(fù)責(zé)Calcite項(xiàng)目的開發(fā)與維護(hù)。
目前, 使用Calcite作為SQL解析與處理引擎有Hive、Drill、Flink、Phoenix和Storm,可以肯定的是還會(huì)有越來越多的數(shù)據(jù)處理引擎采用Calcite作為SQL解析工具。
2. Calcite 主要功能
總結(jié)來說Calcite有以下主要功能:
- SQL 解析
- SQL 校驗(yàn)
- 查詢優(yōu)化
- SQL 生成器
- 數(shù)據(jù)連接
3. Calcite 解析SQl的步驟

如上圖中所述,一般來說Calcite解析SQL有以下幾步:
- Parser. 此步中Calcite通過Java CC將SQL解析成未經(jīng)校驗(yàn)的AST
- Validate. 該步驟主要作用是校證Parser步驟中的AST是否合法,如驗(yàn)證SQL scheme、字段、函數(shù)等是否存在; SQL語句是否合法等. 此步完成之后就生成了RelNode樹(關(guān)于RelNode樹, 請參考下文)
- Optimize. 該步驟主要的作用優(yōu)化RelNode樹, 并將其轉(zhuǎn)化成物理執(zhí)行計(jì)劃。主要涉及SQL規(guī)則優(yōu)化如:基于規(guī)則優(yōu)化(RBO)及基于代價(jià)(CBO)優(yōu)化; Optimze 這一步原則上來說是可選的, 通過Validate后的RelNode樹已經(jīng)可以直接轉(zhuǎn)化物理執(zhí)行計(jì)劃,但現(xiàn)代的SQL解析器基本上都包括有這一步,目的是優(yōu)化SQL執(zhí)行計(jì)劃。此步得到的結(jié)果為物理執(zhí)行計(jì)劃。
- Execute,即執(zhí)行階段。此階段主要做的是:將物理執(zhí)行計(jì)劃轉(zhuǎn)化成可在特定的平臺(tái)執(zhí)行的程序。如Hive與Flink都在在此階段將物理執(zhí)行計(jì)劃CodeGen生成相應(yīng)的可執(zhí)行代碼。
4. Calcite相關(guān)組件
Calcite主要有以下概念:
- Catelog: 主要定義SQL語義相關(guān)的元數(shù)據(jù)與命名空間。
- SQL parser: 主要是把SQL轉(zhuǎn)化成AST.
- SQL validator: 通過Catalog來校證AST.
- Query optimizer: 將AST轉(zhuǎn)化成物理執(zhí)行計(jì)劃、優(yōu)化物理執(zhí)行計(jì)劃.
- SQL generator: 反向?qū)⑽锢韴?zhí)行計(jì)劃轉(zhuǎn)化成SQL語句.
4.1 category
Catalog:主要定義被SQL訪問的命名空間,主要包括以下幾點(diǎn):
- schema: 主要定義schema與表的集合,schame 并不是強(qiáng)制一定需要的,比如說有兩張同名的表T1, T2,就需要schema要區(qū)分這兩張表,如A.T1, B.T1
- 表:對應(yīng)關(guān)系數(shù)據(jù)庫的表,代表一類數(shù)據(jù),在calcite中由
RelDataType定義 -
RelDataType代表表的數(shù)據(jù)定義,如表的數(shù)據(jù)列名稱、類型等。
Schema:
public interface Schema {
Table getTable(String name);
Set<String> getTableNames();
Set<String> getFunctionNames();
Schema getSubSchema(String name);
Set<String> getSubSchemaNames();
Expression getExpression(SchemaPlus parentSchema, String name);
boolean isMutable();
Table:
public interface Table {
RelDataType getRowType(RelDataTypeFactory typeFactory);
Statistic getStatistic();
Schema.TableType getJdbcTableType();
}
其中RelDataType代表Row的數(shù)據(jù)類型, Statistic 用于統(tǒng)計(jì)表的相關(guān)數(shù)據(jù)、特別是在CBO用于計(jì)表計(jì)算表的代價(jià)。
一句Sql
selcct id, name, cast(age as bigint) from A.INFO
-
id, name則為data type field -
bigint為 data type -
A為schema -
INFO為表
4.2 SQL Parser
由Java CC編寫,將SQL轉(zhuǎn)化成AST.
- Java CC 指的是Java Compiler Compiler, 可以將一種特定域相關(guān)的語言轉(zhuǎn)化成Java語言
- 在Calcite中將標(biāo)記(Token)表示為
SqlNode, 并且Sqlnode可以通過unparse方法反向轉(zhuǎn)化成SQL
cast(id as float)
Java CC 可表示為
<CAST>
<LPAREN>
e = Expression(ExprContext.ACCEPT_SUBQUERY)
<AS>
dt = DataType() {agrs.add(dt);}
<RPAREN>
....
4.3 Query Optimizer
首先看一下
INSERT INTO tmp_node
SELECT s1.id1, s1.id2, s2.val1
FROM source1 as s1 INNER JOIN source2 AS s2
ON s1.id1 = s2.id1 and s1.id2 = s2.id2 where s1.val1 > 5 and s2.val2 = 3;
通過Calcite轉(zhuǎn)化為:
LogicalTableModify(table=[[TMP_NODE]], operation=[INSERT], flattened=[false])
LogicalProject(ID1=[$0], ID2=[$1], VAL1=[$7])
LogicalFilter(condition=[AND(>($2, 5), =($8, 3))])
LogicalJoin(condition=[AND(=($0, $5), =($1, $6))], joinType=[INNER])
LogicalTableScan(table=[[SOURCE1]])
LogicalTableScan(table=[[SOURCE2]])
是未經(jīng)優(yōu)化的RelNode樹,可以發(fā)現(xiàn)最底層是TableScan,也是讀取表的原始數(shù)據(jù),緊接著是LogicalJoin,Joiner的類型為INNER JOIN, LogicalJoin之后接下做LogicalFilter 操作,對應(yīng)SQL中的WHERE條件,最后做Project也就是投影操作。
但是我們可以觀察到對于INNER JOIN而言, WHERE 條件是可以下推,如
LogicalTableModify(table=[[TMP_NODE]], operation=[INSERT], flattened=[false])
LogicalProject(ID1=[$0], ID2=[$1], VAL1=[$7])
LogicalJoin(condition=[AND(=($0, $5), =($1, $6))], joinType=[inner])
LogicalFilter(condition=[=($4, 3)])
LogicalProject(ID1=[$0], ID2=[$1], ID3=[$2], VAL1=[$3], VAL2=[$4],VAL3=[$5])
LogicalTableScan(table=[[SOURCE1]])
LogicalFilter(condition=[>($3,5)])
LogicalProject(ID1=[$0], ID2=[$1], ID3=[$2], VAL1=[$3], VAL2=[$4],VAL3=[$5])
LogicalTableScan(table=[[SOURCE2]])
這樣可以減少JOIN的數(shù)據(jù)量,提高SQL效率
實(shí)際過程中可以將JOIN 的中條件下推以較少Join的數(shù)據(jù)量
INSERT INTO tmp_node
SELECT s1.id1, s1.id2, s2.val1
FROM source1 as s1 LEFT JOIN source2 AS s2
ON s1.id1 = s2.id1 and s1.id2 = s2.id2 and s1.id3 = 5
s1.id3 = 5 這個(gè)條件可以先下推過濾s1中的數(shù)據(jù), 但在特定場景下,有些不能下推,如下sql:
INSERT INTO tmp_node
SELECT s1.id1, s1.id2, s2.val1
FROM source1 as s1 LEFT JOIN source2 AS s2
ON s1.id1 = s2.id1 and s1.id2 = s2.id2 and s2.id3 = 5
如果s1,s2是流式表(動(dòng)態(tài)表,請參考Flink流式概念)的話,就不能下推,因?yàn)閟1下推的話,由于過濾后沒有數(shù)據(jù)驅(qū)動(dòng)join操作,因而得不到想要的結(jié)果(詳見Flink/Sparking-Streaming)
那接下來我們可能有一個(gè)疑問,在什么情況下可以做類似下推、上推操作,又是根據(jù)什么原則進(jìn)行的呢?如下圖所示

T1 JOIN T2 JOIN T3
類似于此種情況JOIN的順序是上圖的前者還是后者?這就涉及到Optimizer所使用的方法,Optimizer主要目的就是減小SQL所處理的數(shù)據(jù)量、減少所消耗的資源并最大程度提高SQL執(zhí)行效率如:剪掉無用的列、合并投影、子查詢轉(zhuǎn)化成JOIN、JOIN重排序、下推投影、下推過濾等。目前主要有兩類優(yōu)化方法:基于語法(RBO)與基于代價(jià)(CBO)的優(yōu)化
- RBO(Rule Based Optimization)
通俗一點(diǎn)的話就是事先定義一系列的規(guī)則,然后根據(jù)這些規(guī)則來優(yōu)化執(zhí)行計(jì)劃。
如
-
ProjectFilterRule
此Rule的使用場景為Filter在Project之上,可以將Filter下推。假如某一個(gè)RelNode樹
LogicalFilter
LogicalProject
LogicalTableScan
則可優(yōu)化成
LogicalProject
LogicalFilter
LogicalTableScan
-
FilterJoinRule
此Rule的使用場景為Filter在Join之上,可以先做Filter然后再做Join, 以減少Join的數(shù)量
等等,還有很多類似的規(guī)則。但RBO一定程度上是經(jīng)驗(yàn)試的優(yōu)化方法,無法有一個(gè)公式上的判斷哪種優(yōu)化更優(yōu)。 在Calcite中實(shí)現(xiàn)方法為 HepPlanner
- CBO(Cost Based Optimization)
通俗一點(diǎn)的說法是:通過某種算法計(jì)算SQL所有可能的執(zhí)行計(jì)劃的“代價(jià)”,選擇某一個(gè)代價(jià)較低的執(zhí)行計(jì)劃,如上文中三張表作JOIN, 一般來說RBO無法判斷哪種執(zhí)行計(jì)劃優(yōu)化更好,只有分別計(jì)算每一種JOIN方法的代價(jià)。
Calcite會(huì)將每一種操作(如LogicaJoin、LocialFilter、 LogicalProject、LogicalScan) 結(jié)合實(shí)際的Schema轉(zhuǎn)化成具體的代價(jià)數(shù),比較不同的執(zhí)行計(jì)劃所具有的代價(jià),然后選擇相對小計(jì)劃作為最終的結(jié)果,之所以說相對小,這是因?yàn)槿绻耆闅v計(jì)算所有可能的代價(jià)可能得不償失,花費(fèi)更多的人力與資源,因此只是說選擇相對最優(yōu)的執(zhí)行計(jì)劃。CBO目的是“避免使用最差的執(zhí)行計(jì)劃,而不是找到最好的”
目前Calcite中就是采用CBO進(jìn)行優(yōu)化,實(shí)現(xiàn)方法為VolcanoPlanner,有關(guān)此算法的具體內(nèi)容可以參考原碼
5. 如何使用Calcite
由于Calcite是Java語言編寫,因此只需要在工程或項(xiàng)目中引入相應(yīng)的Jar包即可,下面為一個(gè)可以運(yùn)行的例子:
public class TestOne {
public static class TestSchema {
public final Triple[] rdf = {new Triple("s", "p", "o")};
}
public static void main(String[] args) {
SchemaPlus schemaPlus = Frameworks.createRootSchema(true);
//給schema T中添加表
schemaPlus.add("T", new ReflectiveSchema(new TestSchema()));
Frameworks.ConfigBuilder configBuilder = Frameworks.newConfigBuilder();
//設(shè)置默認(rèn)schema
configBuilder.defaultSchema(schemaPlus);
FrameworkConfig frameworkConfig = configBuilder.build();
SqlParser.ConfigBuilder paresrConfig = SqlParser.configBuilder(frameworkConfig.getParserConfig());
//SQL 大小寫不敏感
paresrConfig.setCaseSensitive(false).setConfig(paresrConfig.build());
Planner planner = Frameworks.getPlanner(frameworkConfig);
SqlNode sqlNode;
RelRoot relRoot = null;
try {
//parser階段
sqlNode = planner.parse("select \"a\".\"s\", count(\"a\".\"s\") from \"T\".\"rdf\" \"a\" group by \"a\".\"s\"");
//validate階段
planner.validate(sqlNode);
//獲取RelNode樹的根
relRoot = planner.rel(sqlNode);
} catch (Exception e) {
e.printStackTrace();
}
RelNode relNode = relRoot.project();
System.out.print(RelOptUtil.toString(relNode));
}
}
類Triple 對應(yīng)的表定義:
public class Triple {
public String s;
public String p;
public String o;
public Triple(String s, String p, String o) {
super();
this.s = s;
this.p = p;
this.o = o;
}
}
詳細(xì)可以代碼在這里
6. Calcite 其它方面
Calcite的功能遠(yuǎn)不止以上介紹,除了標(biāo)準(zhǔn)SQL的,還支持以下內(nèi)容:
- 對流相對概念支持,如在SQL層面支持Window概念,如Session Window, Hopping Window等。
- 支持物化視圖等復(fù)雜概念。
- 獨(dú)立于編程語言和數(shù)據(jù)源,可以支持不同的前端和后端。
7. 總結(jié)
以上內(nèi)容主要介紹上Calcite相關(guān)概念并通過相例子說明了Calcite使用方法, 希望通過上述內(nèi)容,讀者能對Calcite有初步的了解。
由于筆者使用和探索Calcite時(shí)間也不長,以上內(nèi)容難免有錯(cuò)誤與不準(zhǔn)確之處,還望各位讀者不吝指正,相互學(xué)習(xí)。
參考文獻(xiàn)與網(wǎng)址: