2018年发表在SIGMOD的一篇论文我觉得是拿来入门最合适了
优采云 发布时间: 2021-08-10 07:022018年发表在SIGMOD的一篇论文我觉得是拿来入门最合适了
最近一直在关注大数据处理技术和开源产品的实现,发现很多项目都提到了一个叫Apache Calcite的东西。同一件事出现一两次并不奇怪,但在数据处理领域不同时期的产品中被反复提及,必须引起注意。为此,我也搜索了一些资料。 2018年SIGMOD发表的一篇论文,介绍这个东西我觉得最适合入门。以下是我对这篇论文的看法和结论。
这是什么?
解释一下Calcite是什么,论文题目最贴切——A Foundational Framework for Optimized Query Processing Over Heterogeneous Data Sources(优化异构数据源查询处理的基础框架)。 Calcite 提供了标准的 SQL 语言、多种查询优化以及连接各种数据源的能力。从功能上看,它具有数据库管理系统的许多典型功能,如SQL解析、SQL校验、SQL查询优化、SQL生成、数据连接查询等,但不包括DBMS的核心功能例如数据处理和数据存储。另一方面,Calcite 的设计与数据处理和存储无关,这使其成为协调多个数据源和数据处理引擎之间的绝佳选择。
方解石以前称为 optiq。 Optiq 最初用于 Apache Hive 项目中,为 Hive 提供基于成本的优化,即 CBO(cost based optimizations)。 2014年5月,optiq独立,成为Apache社区的孵化项目。 2014年9月,正式更名为Calcite。本项目的目标是一刀切(one size fits all),希望为不同的计算平台和数据源提供统一的查询引擎。
Calcite 的主要功能是 SQL 语法分析(parse)和优化(optimzation)。首先将SQL语句解析成抽象语法树(AST Abstract Syntax Tree),根据一定的规则或成本优化AST算法和关系,最后推送到各个数据处理引擎执行。
为什么
接下来的问题是,为什么我们需要这样一个库来进行 SQL 语法分析和优化?
如果你要自己开发一个分布式计算产品,你必须有类似SQL解析和执行的功能。但是实现这样的功能有一定的技术门槛,需要设计者对关系代数等领域有深入的了解。 SQL分析的结果也需要尽可能与主流的ANSI-SQL保持一致,这样也可以降低公司的推广成本和用户的学习成本。另外,在大数据处理时代的分布式计算场景中,一个SQL往往可以解析成多个语义等价的语法树,但要考虑到不同的数据结构、底层数据处理的规模、操作逻辑等作为内部过滤连接,这些语法树之间的具体执行效率往往相差很大,SQL语句不同,底层执行环境不同,现有选择的优劣也不同。
因此,如何优化这些语法树的执行路径是一个非常重要的课题。在这两点上,大数据处理中的批计算、流计算、交互查询等领域或多或少都存在一些常见的问题。当查询语句背后的关系代数、查询处理和优化被封装和抽象出来,那么就有可能产生一个通用的框架。
如果您是数据用户,您可能会面临集成多个异构数据源(传统关系型数据库、ES等搜索引擎、MongoDB等缓存产品、Spark等分布式计算框架等)的需求。它还可能面临跨平台查询语句分发和执行优化等问题。
定位
于是Apache Calcite应运而生。论文将其定位为一个完整的查询处理系统,但 Calcite 的设计非常灵活。在实际项目中使用一般有两种方式:
使用 Calcite 作为 lib 库并将其嵌入到您自己的项目中。
方解石自有产品系统列表
实现一个适配器(Adapter),项目通过读取数据源的适配器与Calcite集成。
使用 Calcite 适配器的系统列表
功能聚合
DBMS 的五个部分
一般来说,我们可以将一个数据库管理系统分为以上五个部分。在设计之初,Calcite 就确定只需要关注和实现图中蓝色部分,并实现灰色数据管理和数据存储,所有外部计算和存储引擎都实现了。这样做的目的是数据本身的特性导致通常的数据管理和数据存储部分是多样的(文件、关系数据库、列数据库、分布式存储等)和复杂的。 Calcite 放弃了这两个部分,而专注于更通用的上层。模块使系统的复杂性得到有效控制,专注于你能做的、能做的以及能做的更深入更好的领域。
方解石也没有反复制造轮子。当现成的东西可用时,它就可以使用了。比如在SQL解析部分,直接使用开源的JavaCC将SQL语句转化为Java代码,再转化为抽象的语法树,供下一阶段使用。例如,为了实现灵活的元数据功能,Calcite 需要支持 Java 代码的运行时编译,但是默认的 JavaC 太重,需要一个更轻量级的编译器。这里使用的是开源 Janino。
这种功能专注,没有轮子的重新发明,以及足够简单的产品设计思路,让Calcite的实现足够简单稳定。
灵活的可插拔架构
方解石架构
上图是论文中提到的Calcite的架构。 Calcite 的优化器使用关系运算符树作为其内部表示。其内部优化引擎主要由三部分组成:规则、元数据提供者和规划引擎。图中虚线表示方解石与外界的相互作用。从图中可以看出,这种交互的方式有很多种。
图中顶部的 JDBC 客户端代表一个外部应用程序。访问时,通常以SQL语句的形式输入,通过JDBC Client访问Calcite内部的JDBC Server。接下来,JDBC Server 会将传入的SQL 语句通过SQL Parser 和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.Validation(Validate),这一步的主要作用是验证上一步的AST是否合法,比如验证SQL的scheme、field、function等是否存在,SQL语句是否存在是否合法等,完成这一步后生成RelNode树
3.Optimize(优化),这一步的主要作用是优化RelNode树,转化为物理执行计划。通常涉及两种类型的 SQL 规则优化:基于规则的优化 (RBO) 和基于成本的优化 (CBO)。该步骤原则上是可选的。 Validate之后的RelNode树其实可以直接转换物理执行计划。但是现代SQL解析器基本都有这一步,目的是优化SQL执行计划。这一步的结果就是物理执行计划。
4.Execute(执行),这一步主要是将物理执行计划转化为可以在特定平台上执行的程序。例如,Hive 和 Flink 在这个阶段都从物理执行计划 CodeGen 中生成相应的可执行代码。
以下是Calcite的查询Demo示例。我们基于SQL写查询语句,但是内部数据存储不使用任何DB,而是使用存储在JVM内存中的数据。通过这个例子,你可以对Calcite的简单使用有个直观的认识。
maven 介绍
4.0.0
org.study.calcite
demo
1.0-SNAPSHOT
org.apache.calcite
calcite-core
1.19.0
定义架构结构
定义一个Schema结构来表明存储数据的结构是什么样的。该示例定义了一个名为 JavaHrSchema 的模式,可以将其与数据库中的数据库实例进行比较。 Schema中有Employee和Department两个表,可以理解为数据库中的表。该示例最终在内存中为这两个表初始化了一些数据。
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中的数据库访问代码非常相似。写过这段代码的同学一定很熟悉,就不一一介绍了。
<p>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