Apache Calcite 簡介

不想看文章直接訪問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 解析步驟

如上圖中所述,一般來說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):

  1. schema: 主要定義schema與表的集合,schame 并不是強(qiáng)制一定需要的,比如說有兩張同名的表T1, T2,就需要schema要區(qū)分這兩張表,如A.T1, B.T1
  2. 表:對應(yīng)關(guān)系數(shù)據(jù)庫的表,代表一類數(shù)據(jù),在calcite中由RelDataType定義
  3. 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)行的呢?如下圖所示

不同的JOIN順序
T1 JOIN T2 JOIN T3

類似于此種情況JOIN的順序是上圖的前者還是后者?這就涉及到Optimizer所使用的方法,Optimizer主要目的就是減小SQL所處理的數(shù)據(jù)量、減少所消耗的資源并最大程度提高SQL執(zhí)行效率如:剪掉無用的列、合并投影、子查詢轉(zhuǎn)化成JOIN、JOIN重排序、下推投影、下推過濾等。目前主要有兩類優(yōu)化方法:基于語法(RBO)與基于代價(jià)(CBO)的優(yōu)化

  1. 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

  1. 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)址:

  1. http://hbasefly.com/2017/05/04/bigdata%EF%BC%8Dcbo/
  2. http://www.infoq.com/cn/articles/new-big-data-hadoop-query-engine-apache-calcite
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容