開源SQL解析工具-Apache Calcite

概念

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訪問的命名空間,主要包括以下幾點:

  1. schema: 主要定義schema與表的集合,schame 並不是強制一定需要的,比如說有兩張同名的表T1, T2,就需要schema要區分這兩張表,如A.T1, B.T1
  2. 表:對應關係數據庫的表,代表一類數據,在calcite中由RelDataType定義
  3. 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)的優化

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

  1. 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>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章