概念
Apache Calcite 是一款開源SQL解析工具, 可以將各種SQL語句解析成抽象語法術AST(Abstract Syntax Tree), 之後通過操作AST就可以把SQL中所要表達的算法與關係體現在具體代碼之中。
Calcite的生前爲Optiq(也爲Farrago), 爲Java語言編寫, 通過十多年的發展, 在2013年成爲Apache旗下頂級項目,並還在持續發展中, 該項目的創始人爲Julian Hyde, 其擁有多年的SQL引擎開發經驗, 目前在Hortonworks工作, 主要負責Calcite項目的開發與維護。
目前, 使用Calcite作爲SQL解析與處理引擎有Hive、Drill、Flink、Phoenix和Storm,可以肯定的是還會有越來越多的數據處理引擎採用Calcite作爲SQL解析工具。
功能
總結來說Calcite有以下主要功能:
- SQL 解析
- SQL 校驗
- 查詢優化
- SQL 生成器
- 數據連接
Calcite解析SQL步驟
如上圖中所述,一般來說Calcite解析SQL有以下幾步:
- Parser. 此步中Calcite通過Java CC將SQL解析成未經校驗的AST
- Validate. 該步驟主要作用是校證Parser步驟中的AST是否合法,如驗證SQL scheme、字段、函數等是否存在; SQL語句是否合法等. 此步完成之後就生成了RelNode樹(關於RelNode樹, 請參考下文)
- Optimize. 該步驟主要的作用優化RelNode樹, 並將其轉化成物理執行計劃。主要涉及SQL規則優化如:基於規則優化(RBO)及基於代價(CBO)優化; Optimze 這一步原則上來說是可選的, 通過Validate後的RelNode樹已經可以直接轉化物理執行計劃,但現代的SQL解析器基本上都包括有這一步,目的是優化SQL執行計劃。此步得到的結果爲物理執行計劃。
- Execute,即執行階段。此階段主要做的是:將物理執行計劃轉化成可在特定的平臺執行的程序。如Hive與Flink都在在此階段將物理執行計劃CodeGen生成相應的可執行代碼。
Calcite相關組件
Calcite主要有以下概念:
- Catelog: 主要定義SQL語義相關的元數據與命名空間。
- SQL parser: 主要是把SQL轉化成AST.
- SQL validator: 通過Catalog來校證AST.
- Query optimizer: 將AST轉化成物理執行計劃、優化物理執行計劃.
- SQL generator: 反向將物理執行計劃轉化成SQL語句.
1) category
Catalog:主要定義被SQL訪問的命名空間,主要包括以下幾點:
- schema: 主要定義schema與表的集合,schame 並不是強制一定需要的,比如說有兩張同名的表T1, T2,就需要schema要區分這兩張表,如A.T1, B.T1
- 表:對應關係數據庫的表,代表一類數據,在calcite中由
RelDataType
定義 RelDataType
代表表的數據定義,如表的數據列名稱、類型等。
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的數據類型, Statistic 用於統計表的相關數據、特別是在CBO用於計表計算表的代價。
2) SQL Parser
由Java CC編寫,將SQL轉化成AST.
- Java CC 指的是Java Compiler Compiler, 可以將一種特定域相關的語言轉化成Java語言
- 在Calcite中將標記(Token)表示爲
SqlNode
, 並且Sqlnode
可以通過unparse
方法反向轉化成SQL
cast(id as float)
Java CC 可表示爲
<CAST>
<LPAREN>
e = Expression(ExprContext.ACCEPT_SUBQUERY)
<AS>
dt = DataType() {agrs.add(dt);}
<RPAREN>
....
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轉化爲:
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]])
是未經優化的RelNode樹,可以發現最底層是TableScan,也是讀取表的原始數據,緊接着是LogicalJoin,Joiner的類型爲INNER JOIN, LogicalJoin之後接下做LogicalFilter 操作,對應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的數據量,提高SQL效率
實際過程中可以將JOIN 的中條件下推以較少Join的數據量
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
這個條件可以先下推過濾s1中的數據, 但在特定場景下,有些不能下推,如下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是流式表(動態表,請參考Flink流式概念)的話,就不能下推,因爲s1下推的話,由於過濾後沒有數據驅動join操作,因而得不到想要的結果(詳見Flink/Sparking-Streaming)
那接下來我們可能有一個疑問,在什麼情況下可以做類似下推、上推操作,又是根據什麼原則進行的呢?如下圖所示
T1 JOIN T2 JOIN T3
類似於此種情況JOIN的順序是上圖的前者還是後者?這就涉及到Optimizer所使用的方法,Optimizer主要目的就是減小SQL所處理的數據量、減少所消耗的資源並最大程度提高SQL執行效率如:剪掉無用的列、合併投影、子查詢轉化成JOIN、JOIN重排序、下推投影、下推過濾等。目前主要有兩類優化方法:基於語法(RBO)與基於代價(CBO)的優化
- RBO(Rule Based Optimization)
通俗一點的話就是事先定義一系列的規則,然後根據這些規則來優化執行計劃。
如
-
ProjectFilterRule
此Rule的使用場景爲Filter在Project之上,可以將Filter下推。假如某一個RelNode樹
LogicalFilter
LogicalProject
LogicalTableScan
則可優化成
LogicalProject
LogicalFilter
LogicalTableScan
-
FilterJoinRule
此Rule的使用場景爲Filter在Join之上,可以先做Filter然後再做Join, 以減少Join的數量
等等,還有很多類似的規則。但RBO一定程度上是經驗試的優化方法,無法有一個公式上的判斷哪種優化更優。 在Calcite中實現方法爲 HepPlanner
- CBO(Cost Based Optimization)
通俗一點的說法是:通過某種算法計算SQL所有可能的執行計劃的“代價”,選擇某一個代價較低的執行計劃,如上文中三張表作JOIN, 一般來說RBO無法判斷哪種執行計劃優化更好,只有分別計算每一種JOIN方法的代價。
Calcite會將每一種操作(如LogicaJoin、LocialFilter、 LogicalProject、LogicalScan) 結合實際的Schema轉化成具體的代價數,比較不同的執行計劃所具有的代價,然後選擇相對小計劃作爲最終的結果,之所以說相對小,這是因爲如果要完全遍歷計算所有可能的代價可能得不償失,花費更多的人力與資源,因此只是說選擇相對最優的執行計劃。CBO目的是“避免使用最差的執行計劃,而不是找到最好的”
目前Calcite中就是採用CBO進行優化,實現方法爲VolcanoPlanner
,有關此算法的具體內容可以參考原碼
Calcite使用
由於Calcite是Java語言編寫,因此只需要在工程或項目中引入相應的Jar包即可,下面爲一個可以運行的例子:
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();
//設置默認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 對應的表定義:
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;
}
}
具體代碼參見:https://github.com/yuqi1129/calcite-test
Calcite 使用Mysql Demo
配置calcite查詢本地mysql中dbtest_1 數據庫下student表
model json如下:
{
version: '1.0',
defaultSchema: 'dbtest_1',
schemas: [
{
name: 'dbtest_1',
type: 'custom',
factory: 'org.apache.calcite.adapter.jdbc.JdbcSchema$Factory',
operand: {
jdbcDriver: 'com.mysql.jdbc.Driver',
jdbcUrl: 'jdbc:mysql://localhost:3306/dbtest_1',
jdbcUser: 'xxx',
jdbcPassword: 'xxx'
}
}
]
}
測試代碼:
package com.learn.mysql;
import org.apache.calcite.jdbc.CalciteConnection;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
/**
* @Description:
* @Date:Create in 下午5:38 2018/9/15
* @Modified By:
*/
public class QueryMysql {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Class.forName("com.mysql.jdbc.Driver");
Properties info = new Properties();
info.put("model",
"inline:"
+ "{\n"
+ " version: '1.0',\n"
+ " defaultSchema: 'dbtest_1',\n"
+ " schemas: [\n"
+ " {\n"
+ " name: 'dbtest_1',\n"
+ " type: 'custom',\n"
+ " factory: 'org.apache.calcite.adapter.jdbc.JdbcSchema$Factory',\n"
+ " operand: {\n"
+ " jdbcDriver: 'com.mysql.jdbc.Driver',\n"
+ " jdbcUrl:'jdbc:mysql://localhost:3306/dbtest_1',\n"
+ " jdbcUser: 'xxx',\n"
+ " jdbcPassword: 'xxx'\n"
+ " }\n"
+ " }\n"
+ " ]\n"
+ "}");
Connection connection =
DriverManager.getConnection("jdbc:calcite:", info);
// must print "directory ... not found" to stdout, but not fail
Statement statement = connection.createStatement();
CalciteConnection calciteConnection =
connection.unwrap(CalciteConnection.class);
ResultSet resultSet =
statement.executeQuery("select * from \"dbtest_1\".\"student\"");
ResultSet tables =
connection.getMetaData().getTables(null, null, null, null);
final StringBuilder buf = new StringBuilder();
while (resultSet.next()) {
int n = resultSet.getMetaData().getColumnCount();
for (int i = 1; i <= n; i++) {
buf.append(i > 1 ? "; " : "")
.append(resultSet.getMetaData().getColumnLabel(i))
.append("=")
.append(resultSet.getObject(i));
}
System.out.println(buf.toString());
buf.setLength(0);
}
resultSet.close();
statement.close();
connection.close();
}
}
結果
id=1; age=18; name=李四
id=2; age=18; name=張三
id=4; age=12; name=studentA
依賴
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.apache.calcite</groupId>
<artifactId>calcite-core</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.calcite.avatica</groupId>
<artifactId>avatica-core</artifactId>
</exclusion>
</exclusions>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.calcite.avatica/avatica -->
<dependency>
<groupId>org.apache.calcite.avatica</groupId>
<artifactId>avatica</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
</dependencies>