最近在關(guān)注大數(shù)據(jù)處理的技術(shù)和開源產(chǎn)品的實(shí)現(xiàn),發(fā)現(xiàn)很多項(xiàng)目中都提到了一個叫 Apache Calcite 的東西。同樣的東西一兩次見不足為奇,可再三被數(shù)據(jù)處理領(lǐng)域的各個不同時期的產(chǎn)品提到就必須引起注意了。為此也搜了些資料,關(guān)于這個東西的介紹2018 年發(fā)表在 SIGMOD 的一篇論文我覺得是拿來入門最合適了,以下是我關(guān)于這篇論文的思考和總結(jié)。
是什么
解釋 Calcite 是什么,用論文的標(biāo)題是最合適了—— A Foundational Framework for Optimized Query Processing Over Heterogeneous Data Sources(一個用于優(yōu)化異構(gòu)數(shù)據(jù)源的查詢處理的基礎(chǔ)框架)。Calcite 提供了標(biāo)準(zhǔn)的 SQL 語言、多種查詢優(yōu)化和連接各種數(shù)據(jù)源的能力。從功能上看它有很多數(shù)據(jù)庫管理系統(tǒng)的典型功能,比如 SQL 解析、SQL 校驗(yàn)、SQL 查詢優(yōu)化、SQL 生成、數(shù)據(jù)連接查詢等等,但卻不包括數(shù)據(jù)處理、數(shù)據(jù)存儲等 DBMS 的核心功能。從另一方面看,正因?yàn)?Calcite 這種與數(shù)據(jù)處理和存儲的無關(guān)的設(shè)計(jì),才使它成為在多個數(shù)據(jù)源和數(shù)據(jù)處理引擎之間進(jìn)行協(xié)調(diào)的絕佳選擇。
Calcite 之前叫做 optiq,optiq 起初用于 Apache Hive 項(xiàng)目中,為 Hive 提供基于成本模型的優(yōu)化,即CBO(cost based optimizations)。2014 年 5 月 optiq 獨(dú)立出來,成為 Apache 社區(qū)的孵化項(xiàng)目,2014 年 9 月正式更名為 Calcite。該項(xiàng)目的目標(biāo)是 one size fits all(一種方案適應(yīng)所有需求場景),希望能為不同計(jì)算平臺和數(shù)據(jù)源提供統(tǒng)一的查詢引擎。
Calcite 的主要功能是 SQL 語法解析(parse)和優(yōu)化(optimazation)。首先它會把 SQL 語句解析成抽象語法樹(AST Abstract Syntax Tree),并基于一定規(guī)則或成本對 AST 的算法與關(guān)系進(jìn)行優(yōu)化,最后推給各個數(shù)據(jù)處理引擎進(jìn)行執(zhí)行。
為什么
接下來的問題就是,我們?yōu)槭裁葱枰@么一個 SQL 語法解析和優(yōu)化的庫呢?
如果你準(zhǔn)備自研一個分布式計(jì)算產(chǎn)品,肯定少不了類似 SQL 解析、執(zhí)行的功能,而實(shí)現(xiàn)此類功能則存在一定技術(shù)門檻,需要設(shè)計(jì)者對關(guān)系代數(shù)等領(lǐng)域有比較深的理解。SQL 解析的結(jié)果也需要盡量和主流的 ANSI-SQL 一致,這樣也能降低公司的推廣成本、使用者的學(xué)習(xí)成本。此外,大數(shù)據(jù)處理時代的分布式計(jì)算場景下,往往一條 SQL 可以解析成多棵語義對等的語法樹,但考慮到不同數(shù)據(jù)結(jié)構(gòu)、底層數(shù)據(jù)處理的量級、內(nèi)部的過濾連接等操作的邏輯,這些語法樹之間的具體執(zhí)行效率往往差別很大,SQL 語句不同,底層的執(zhí)行環(huán)境不同,存在的優(yōu)劣選擇也各不相同。
因此,怎么優(yōu)化這些語法樹的執(zhí)行路徑就是一個非常重要的課題。在這兩點(diǎn)上,大數(shù)據(jù)處理中的批量計(jì)算、流計(jì)算、交互查詢等領(lǐng)域多多少少都會存在一些共性問題,當(dāng)把查詢語句背后的關(guān)系代數(shù)、查詢處理和優(yōu)化等問題封裝抽象之后,則有產(chǎn)生一個通用框架的可能。
如果你是一個數(shù)據(jù)使用者,可能會面臨多種異構(gòu)數(shù)據(jù)源需要整合(有傳統(tǒng)的關(guān)系數(shù)據(jù)庫、搜索引擎如 ES、緩存產(chǎn)品如 MongoDB、分布式計(jì)算框架如 Spark 等等),此時同樣可能面臨跨平臺的查詢語句分發(fā)及執(zhí)行優(yōu)化等課題。
定位
因此 Apache Calcite 應(yīng)運(yùn)而生,論文里把它定位為一個完整的查詢處理系統(tǒng),但 Calcite 的設(shè)計(jì)是非常靈活,實(shí)際項(xiàng)目中一般有兩種使用方式:
-
把 Calcite 當(dāng)作 lib 庫,嵌入到自己的項(xiàng)目中。
把 Calcite 的自己產(chǎn)品的系統(tǒng)列表 -
實(shí)現(xiàn)一個適配器(Adapter),項(xiàng)目通過讀取數(shù)據(jù)源的適配器與 Calcite 集成。
采用 Calcite 適配器的系統(tǒng)列表
功能聚集

一般來說,我們可以把一個數(shù)據(jù)庫管理系統(tǒng)分為如上五部分, Calcite 在設(shè)計(jì)之初就確定了只關(guān)注和實(shí)現(xiàn)圖中藍(lán)色三部分,而把灰色的數(shù)據(jù)管理與數(shù)據(jù)存儲開放給各外部計(jì)算、存儲引擎來實(shí)現(xiàn)。這樣做的目的是數(shù)據(jù)本身的特性導(dǎo)致通常數(shù)據(jù)管理和數(shù)據(jù)存儲部分即多樣(文件、關(guān)系數(shù)據(jù)庫、列數(shù)據(jù)庫、分布式存儲等等)又復(fù)雜,Calcite 放棄了這兩部分而專注于上層更通用的模塊,使系統(tǒng)的復(fù)雜性得到有效控制,聚焦于自己能做、會做、可以做得更深更好的領(lǐng)域。
Calcite 也沒有重復(fù)去造輪子,有現(xiàn)成東西可用時拿來即用,比如在 SQL 解析這一部分就直接使用了開源的 JavaCC 將 SQL 語句轉(zhuǎn)化為 Java 代碼,再轉(zhuǎn)換成一顆抽象語法樹供下一階段使用。又比如為了實(shí)現(xiàn)靈活的元數(shù)據(jù)功能,Calcite 需要支持運(yùn)行時編譯 Java 代碼,而默認(rèn)的 JavaC 太重,需要一個更輕量級的編譯器,這里就用了開源的 Janino 。
這種功能聚焦、不重復(fù)造輪子、足夠簡單的產(chǎn)品設(shè)計(jì)思路使 Calcite 的實(shí)現(xiàn)足夠簡單和穩(wěn)定。
靈活可插拔架構(gòu)

上圖是論文中提到的 Calcite 的架構(gòu),Calcite 的優(yōu)化器使用關(guān)系運(yùn)算符樹作為其內(nèi)部表示,其內(nèi)部優(yōu)化引擎主要由三個組件組成:規(guī)則、元數(shù)據(jù)提供者和規(guī)劃引擎。圖中虛線表示 Calcite 與外部的相互作用,從圖中可看出這種相互作用的方式有多種。
圖中最上面的 JDBC client 表示外部的應(yīng)用,訪問時一般會以 SQL 語句的形式輸入,通過 JDBC Client 訪問 Calcite 內(nèi)部的 JDBC Server 。接下來 JDBC Server 會把傳入的 SQL 語句經(jīng)過 SQL Parser and Validator 模塊做 SQL 的解析和校驗(yàn),而旁邊的 Expressions Builder 用于支持 Calcite 做 SQL 解析和校驗(yàn)的框架對接。再接著是 Operator Expressions 模塊來處理關(guān)系表達(dá)式,Metadata Providers 用來支持外部自定義元數(shù)據(jù),Pluggable Rules 用來定義優(yōu)化規(guī)則,最核心的 Query Optimizer 則專注查詢優(yōu)化。
Calcite 內(nèi)部包含了一個查詢解析器和驗(yàn)證器,它可將 SQL 查詢轉(zhuǎn)換為關(guān)系運(yùn)算符樹。由于 Calcite 不包含數(shù)據(jù)存儲層,它提供了一種機(jī)制,通過適配器的方式在外部存儲引擎中定義表和視圖等,因此 Calcite 可以用在這些存儲引擎的上層。Calcite 不僅可以為數(shù)據(jù)庫語言支持的系統(tǒng)提供 SQL 優(yōu)化,還為已經(jīng)擁有自己語言解析和解釋的系統(tǒng)提供優(yōu)化支持。
由于功能模塊劃分比較獨(dú)立、合理,Calcite 可以不用全部集成,它允許你只選擇集成和使用其中的一部分功能?;旧厦總€模塊也都支持自定義,這就讓用戶能實(shí)現(xiàn)更靈活的功能定制。
怎么做
一般來說 Calcite 解析 SQL 有下面幾步:
1.解析(Parser),Calcite 通過Java CC 將 SQL 解析成未經(jīng)校驗(yàn)的的 AST
2.驗(yàn)證(Validate),該步主要作用是校驗(yàn)上一步中的 AST 是否合法,比如如驗(yàn)證 SQL scheme、字段、函數(shù)等是否存在,SQL 語句是否合法等等,此步完成之后就生成了 RelNode 樹
3.優(yōu)化(Optimize),該步主要作用是優(yōu)化 RelNode 樹,把它轉(zhuǎn)化成物理執(zhí)行計(jì)劃。涉及的 SQL 規(guī)則優(yōu)化一般有兩種:基于規(guī)則的優(yōu)化(RBO)、基于成本的優(yōu)化(CBO)這一步原則上說是可選的,經(jīng)過 Validate 后的 RelNode 樹實(shí)際就可以直接轉(zhuǎn)化物理執(zhí)行計(jì)劃,但現(xiàn)代的 SQL 解析器基本上都有這一步,目的是優(yōu)化 SQL 執(zhí)行計(jì)劃。該步驟得到的結(jié)果是物理執(zhí)行計(jì)劃。
4.執(zhí)行(Execute),這一步主要做的是把物理執(zhí)行計(jì)劃轉(zhuǎn)換成可在特定平臺執(zhí)行的程序。如 Hive 、Flink 都在此階段將物理執(zhí)行計(jì)劃 CodeGen 生成相應(yīng)的可執(zhí)行代碼。
下面是 Calcite 的一個查詢 Demo 的例子,我們仿照 SQL 寫一條查詢語句,但內(nèi)部數(shù)據(jù)存儲并沒有用任何 DB,而是用的 JVM 內(nèi)存存放的數(shù)據(jù)。通過這個示例可以對 Calcite 的簡單使用有一個直觀感知。
maven 引入
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.study.calcite</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--calcite 核心-->
<dependency>
<groupId>org.apache.calcite</groupId>
<artifactId>calcite-core</artifactId>
<version>1.19.0</version>
</dependency>
</dependencies>
</project>
定義 Schema 結(jié)構(gòu)
定義一個 Schema 結(jié)構(gòu)用于表示存放數(shù)據(jù)的結(jié)構(gòu)是什么樣子的,示例中定義了一個叫 JavaHrSchema 的 schema ,可以把它類比成數(shù)據(jù)庫里面的一個 DB 實(shí)例。該 Schema 內(nèi)有 Employee 和 Department 兩張 table ,可以把它們理解成數(shù)據(jù)庫里的表,示例最后在內(nèi)存里給這兩張表初始化了一些數(shù)據(jù)。
package org.study.calcite.demo.inmemory;
/**
* 定義 Schema 結(jié)構(gòu)
*
* @author niwei
*/
public class JavaHrSchema {
public static class Employee {
public final int emp_id;
public final String name;
public final int dept_no;
public Employee(int emp_id, String name, int dept_no) {
this.emp_id = emp_id;
this.name = name;
this.dept_no = dept_no;
}
}
public static class Department {
public final String name;
public final int dept_no;
public Department(int dept_no, String name) {
this.dept_no = dept_no;
this.name = name;
}
}
public final Employee[] employee = {
new Employee(100, "joe", 1),
new Employee(200, "oliver", 2),
new Employee(300, "twist", 1),
new Employee(301, "king", 3),
new Employee(305, "kelly", 1)
};
public final Department[] department = {
new Department(1, "dev"),
new Department(2, "market"),
new Department(3, "test")
};
}
Java 代碼示例
接下來就是寫一條 SQL 語句并執(zhí)行了,要做這些事情前提是告訴 Calcite 當(dāng)前要操作的 Schema 、 Table 的定義,這就需要給 Calcite 添加數(shù)據(jù)源。從 Calcite 提供的 API 來看其實(shí)和 JDBC 里的數(shù)據(jù)庫訪問代碼很類似,寫過這種代碼的同學(xué)肯定很熟,就不一一介紹了。
package org.study.calcite.demo.inmemory;
import org.apache.calcite.adapter.java.ReflectiveSchema;
import org.apache.calcite.jdbc.CalciteConnection;
import org.apache.calcite.schema.SchemaPlus;
import java.sql.*;
import java.util.Properties;
public class QueryDemo {
public static void main(String[] args) throws Exception {
Class.forName("org.apache.calcite.jdbc.Driver");
Properties info = new Properties();
info.setProperty("lex", "JAVA");
Connection connection = DriverManager.getConnection("jdbc:calcite:", info);
CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class);
SchemaPlus rootSchema = calciteConnection.getRootSchema();
/**
* 注冊一個對象作為 schema ,通過反射讀取 JavaHrSchema 對象內(nèi)部結(jié)構(gòu),將其屬性 employee 和 department 作為表
*/
rootSchema.add("hr", new ReflectiveSchema(new JavaHrSchema()));
Statement statement = calciteConnection.createStatement();
ResultSet resultSet = statement.executeQuery(
"select e.emp_id, e.name as emp_name, e.dept_no, d.name as dept_name "
+ "from hr.employee as e "
+ "left join hr.department as d on e.dept_no = d.dept_no");
/**
* 遍歷 SQL 執(zhí)行結(jié)果
*/
while (resultSet.next()) {
for (int i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) {
System.out.print(resultSet.getMetaData().getColumnName(i) + ":" + resultSet.getObject(i));
System.out.print(" | ");
}
System.out.println();
}
resultSet.close();
statement.close();
connection.close();
}
}

