Apache Calcite 論文學習筆記

最近在關注大數據處理的技術和開源產品的實現,發現不少項目中都提到了一個叫 Apache Calcite 的東西。一樣的東西一兩次見不足爲奇,可再三被數據處理領域的各個不一樣時期的產品提到就必須引發注意了。爲此也搜了些資料,關於這個東西的介紹2018 年發表在 SIGMOD 的一篇論文我以爲是拿來入門最合適了,如下是我關於這篇論文的思考和總結。java

是什麼

解釋 Calcite 是什麼,用論文的標題是最合適了—— A Foundational Framework for Optimized Query Processing Over Heterogeneous Data Sources(一個用於優化異構數據源的查詢處理的基礎框架)。Calcite 提供了標準的 SQL 語言、多種查詢優化和鏈接各類數據源的能力。從功能上看它有不少數據庫管理系統的典型功能,好比 SQL 解析、SQL 校驗、SQL 查詢優化、SQL 生成、數據鏈接查詢等等,但卻不包括數據處理、數據存儲等 DBMS 的核心功能。從另外一方面看,正由於 Calcite 這種與數據處理和存儲的無關的設計,才使它成爲在多個數據源和數據處理引擎之間進行協調的絕佳選擇。算法

Calcite 以前叫作 optiq,optiq 起初用於 Apache Hive 項目中,爲 Hive 提供基於成本模型的優化,即CBO(cost based optimizations)。2014 年 5 月 optiq 獨立出來,成爲 Apache 社區的孵化項目,2014 年 9 月正式改名爲 Calcite。該項目的目標是 one size fits all(一種方案適應全部需求場景),但願能爲不一樣計算平臺和數據源提供統一的查詢引擎。sql

Calcite 的主要功能是 SQL 語法解析(parse)和優化(optimazation)。首先它會把 SQL 語句解析成抽象語法樹(AST Abstract Syntax Tree),並基於必定規則或成本對 AST 的算法與關係進行優化,最後推給各個數據處理引擎進行執行。數據庫

爲何

接下來的問題就是,咱們爲何須要這麼一個 SQL 語法解析和優化的庫呢?apache

若是你準備自研一個分佈式計算產品,確定少不了相似 SQL 解析、執行的功能,而實現此類功能則存在必定技術門檻,須要設計者對關係代數等領域有比較深的理解。SQL 解析的結果也須要儘可能和主流的 ANSI-SQL 一致,這樣也能下降公司的推廣成本、使用者的學習成本。此外,大數據處理時代的分佈式計算場景下,每每一條 SQL 能夠解析成多棵語義對等的語法樹,但考慮到不一樣數據結構、底層數據處理的量級、內部的過濾鏈接等操做的邏輯,這些語法樹之間的具體執行效率每每差異很大,SQL 語句不一樣,底層的執行環境不一樣,存在的優劣選擇也各不相同。緩存

所以,怎麼優化這些語法樹的執行路徑就是一個很是重要的課題。在這兩點上,大數據處理中的批量計算、流計算、交互查詢等領域多多少少都會存在一些共性問題,當把查詢語句背後的關係代數、查詢處理和優化等問題封裝抽象以後,則有產生一個通用框架的可能。bash

若是你是一個數據使用者,可能會面臨多種異構數據源須要整合(有傳統的關係數據庫、搜索引擎如 ES、緩存產品如 MongoDB、分佈式計算框架如 Spark 等等),此時一樣可能面臨跨平臺的查詢語句分發及執行優化等課題。數據結構

定位

所以 Apache Calcite 應運而生,論文裏把它定位爲一個完整的查詢處理系統,但 Calcite 的設計是很是靈活,實際項目中通常有兩種使用方式:架構

  1. 把 Calcite 看成 lib 庫,嵌入到本身的項目中。 框架

    把 Calcite 的本身產品的系統列表

  2. 實現一個適配器(Adapter),項目經過讀取數據源的適配器與 Calcite 集成。

    採用 Calcite 適配器的系統列表

功能彙集

DBMS 五部分

通常來講,咱們能夠把一個數據庫管理系統分爲如上五部分, Calcite 在設計之初就肯定了只關注和實現圖中藍色三部分,而把灰色的數據管理與數據存儲開放給各外部計算、存儲引擎來實現。這樣作的目的是數據自己的特性致使一般數據管理和數據存儲部分即多樣(文件、關係數據庫、列數據庫、分佈式存儲等等)又複雜,Calcite 放棄了這兩部分而專一於上層更通用的模塊,使系統的複雜性獲得有效控制,聚焦於本身能作、會作、能夠作得更深更好的領域。

Calcite 也沒有重複去造輪子,有現成東西可用時拿來即用,好比在 SQL 解析這一部分就直接使用了開源的 JavaCC 將 SQL 語句轉化爲 Java 代碼,再轉換成一顆抽象語法樹供下一階段使用。又好比爲了實現靈活的元數據功能,Calcite 須要支持運行時編譯 Java 代碼,而默認的 JavaC 過重,須要一個更輕量級的編譯器,這裏就用了開源的 Janino 。

這種功能聚焦、不重複造輪子、足夠簡單的產品設計思路使 Calcite 的實現足夠簡單和穩定。

靈活可插拔架構

Calcite 架構

上圖是論文中提到的 Calcite 的架構,Calcite 的優化器使用關係運算符樹做爲其內部表示,其內部優化引擎主要由三個組件組成:規則、元數據提供者和規劃引擎。圖中虛線表示 Calcite 與外部的相互做用,從圖中可看出這種相互做用的方式有多種。

圖中最上面的 JDBC client 表示外部的應用,訪問時通常會以 SQL 語句的形式輸入,經過 JDBC Client 訪問 Calcite 內部的 JDBC Server 。接下來 JDBC Server 會把傳入的 SQL 語句通過 SQL Parser and Validator 模塊作 SQL 的解析和校驗,而旁邊的 Expressions Builder 用於支持 Calcite 作 SQL 解析和校驗的框架對接。再接着是 Operator Expressions 模塊來處理關係表達式,Metadata Providers 用來支持外部自定義元數據,Pluggable Rules 用來定義優化規則,最核心的 Query Optimizer 則專一查詢優化。

Calcite 內部包含了一個查詢解析器和驗證器,它可將 SQL 查詢轉換爲關係運算符樹。因爲 Calcite 不包含數據存儲層,它提供了一種機制,經過適配器的方式在外部存儲引擎中定義表和視圖等,所以 Calcite 能夠用在這些存儲引擎的上層。Calcite 不只能夠爲數據庫語言支持的系統提供 SQL 優化,還爲已經擁有本身語言解析和解釋的系統提供優化支持。

因爲功能模塊劃分比較獨立、合理,Calcite 能夠不用所有集成,它容許你只選擇集成和使用其中的一部分功能。基本上每一個模塊也都支持自定義,這就讓用戶能實現更靈活的功能定製。

怎麼作

通常來講 Calcite 解析 SQL 有下面幾步: 1.解析(Parser),Calcite 經過Java CC 將 SQL 解析成未經校驗的的 AST 2.驗證(Validate),該步主要做用是校驗上一步中的 AST 是否合法,好比如驗證 SQL scheme、字段、函數等是否存在,SQL 語句是否合法等等,此步完成以後就生成了 RelNode 樹 3.優化(Optimize),該步主要做用是優化 RelNode 樹,把它轉化成物理執行計劃。涉及的 SQL 規則優化通常有兩種:基於規則的優化(RBO)、基於成本的優化(CBO)這一步原則上說是可選的,通過 Validate 後的 RelNode 樹實際就能夠直接轉化物理執行計劃,但現代的 SQL 解析器基本上都有這一步,目的是優化 SQL 執行計劃。該步驟獲得的結果是物理執行計劃。 4.執行(Execute),這一步主要作的是把物理執行計劃轉換成可在特定平臺執行的程序。如 Hive 、Flink 都在此階段將物理執行計劃 CodeGen 生成相應的可執行代碼。

下面是 Calcite 的一個查詢 Demo 的例子,咱們仿照 SQL 寫一條查詢語句,但內部數據存儲並無用任何 DB,而是用的 JVM 內存存放的數據。經過這個示例能夠對 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 結構

定義一個 Schema 結構用於表示存放數據的結構是什麼樣子的,示例中定義了一個叫 JavaHrSchema 的 schema ,能夠把它類比成數據庫裏面的一個 DB 實例。該 Schema 內有 Employee 和 Department 兩張 table ,能夠把它們理解成數據庫裏的表,示例最後在內存裏給這兩張表初始化了一些數據。

package org.study.calcite.demo.inmemory;

/**
 * 定義 Schema 結構
 *
 * @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 語句並執行了,要作這些事情前提是告訴 Calcite 當前要操做的 Schema 、 Table 的定義,這就須要給 Calcite 添加數據源。從 Calcite 提供的 API 來看其實和 JDBC 裏的數據庫訪問代碼很相似,寫過這種代碼的同窗確定很熟,就不一一介紹了。

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 對象內部結構,將其屬性 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 執行結果
         */
        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();

    }
}
複製代碼
相關文章
相關標籤/搜索