Calcite-学习笔记(入门篇)

目录

参考文章

什么是Apache Calcite?

为什么需要Apache Calcite?

什么是关系代数Relational algebra ?

什么是数据库系统的查询优化?

Calcite架构

Calcited源码的核心概念

Calcite 解析SQl的步骤

Calcite 解析SQl的步骤(伪代码版)

(1)解析SQL ,即SQL➡️SqlNode

(2)语法检查,即SqlNode➡️SqlNode

(3)语义分析,即SqlNode➡️RelNode/RexNode

(4)逻辑计划优化,即RelNode➡️RelNode

4. Calcite相关组件

4.1 category

4.2 SQL Parser

4.3 Query Optimizer

5. 如何使用Calcite

Calcite代码实战

6. Calcite 其它方面

7. 总结


参考文章

目的链接
Apache Calcite 论文

Apache Calcite: A Foundational Framework for Optimized Query Processing Over Heterogeneous Data Sources

Apache Calcite | Proceedings of the 2018 International Conference on Management of Data (acm.org)

Apache Calcite 简介

链接:https://www.jianshu.com/p/2dfbd71b7f0f
大佬的汇总【必看】

Apache Calcite 学习 (一) - 青紫天涯 - 博客园 (cnblogs.com)

apache calcite 优化器(二) - 青紫天涯 - 博客园 (cnblogs.com)

大佬的汇总【必看】

Apache Calcite整体架构及处理流程 - Liebing's Blog

Apache Calcite原理与实践 - Liebing's Blog

初探calcite【Calcite】Apache Calcite 框架初探及概念详解_董嘻嘻的博客-CSDN博客_apache calcite
关系代数

关系代数(Relational Algebra)概览_zsi386的博客-CSDN博客_关系代数

SQL 形式化语言——关系代数_QuinnNorris的博客-CSDN博客_sql关系代数表达式

关系代数【国内教科书版】

【数据库原理】关系代数_Anova.YJ的博客-CSDN博客_关系代数

吐槽:看着就头疼

关系代数-除法运算关系代数运算——除法运算_g-Jack的博客-CSDN博客_关系代数除法

什么是Apache Calcite?

(1)简介

        解释 Apache Calcite 是什么,用论文的标题是最合适:A Foundational Framework for Optimized Query Processing Over Heterogeneous Data Sources(一个用于优化异构数据源的查询处理的基础框架)。Calcite 提供标准的 SQL 语言、多种查询优化和连接各种数据源的能力。从功能上看它有很多数据库管理系统的典型功能,比如 SQL 解析、SQL 校验、SQL 查询优化、SQL 生成、数据连接查询、OLAP 和流处理的查询引擎等等,但却不包括数据处理、数据存储等 DBMS 的核心功能。从另一方面看,正因为 Calcite 这种与数据处理和存储的无关的设计,才使它成为在多个数据源和数据处理引擎之间进行协调的绝佳选择。

        Calcite 的主要功能是 SQL 语法解析(parse)优化(optimazation)。首先它会把 SQL 语句解析成抽象语法树(AST Abstract Syntax Tree),并基于一定规则或成本对 AST 的算法与关系进行优化,最后推给各个数据处理引擎进行执行。

         Calcite 之前的名称 optiq (也称为Farrago, 使用Java语言编写)。optiq 起初在 Hive 项目中,为 Hive 提供基于成本模型CBO 的优化。2014 年 5 月 optiq 独立出来,成为 Apache 社区的孵化项目,2014 年 9 月正式更名为 Calcite。Calcite 项目的创建者是 Julian Hyde ,他在数据平台上有非常多的工作经历,曾经是 Oracle、 Broadbase 公司 SQL 引擎的主要开发者、SQLStream 公司的创始人和主架构师、Pentaho BI 套件中 OLAP 部分的架构师和主要开发者。现在他在 Hortonworks 公司负责 Calcite 项目,其工作经历对 Calcite 项目有很大的帮助。除了 Hortonworks,该项目的代码提交者还有 MapR 、Salesforce 等公司,并且还在不断壮大。Calcite 的目标是“ one size fits all (一种方案适应所有需求场景)”,希望能为不同计算平台和数据源提供统一的查询引擎,并以类似传统数据库的访问方式(SQL 和高级查询优化)来访问Hadoop 等数据处理系统(DataProcessingSystem)。

        目前, 使用Calcite作为SQL解析与处理引擎有Hive、Drill、Flink、Phoenix和Storm,还会有越来越多的数据处理引擎采用Calcite作为SQL解析工具。

(2)技术特性

  1. 支持标准 SQL 语言;
  2. 独立于编程语言和数据源, 可以支持不同的前端和后端;
  3. 支持关系代数(relational algebra)、可定制的逻辑规划规则和基于成本模型(CBO、Cost Based Optimizatio)优化的查询引擎;
  4. 支持物化视图(materialized view)的管理:创建、丢弃、持久化和自动识别;
  5. 灵活性、组件可插拔、可扩展(flexible, embeddable, and extensible)。它的 SQL Parser 层、Optimizer 层等都可以单独使用。

不想看文章直接访问mysql-protocal(Java版本的Mysql)、calcite-test,这里有关于Calcite RBO,CBO使用具体用例


为什么需要Apache Calcite?

接下来的问题:为什么需要这么一个SQL语法解析和优化的库呢?

        如果你准备自研一个分布式计算产品,肯定少不了类似 SQL 解析、执行的功能,而实现此类功能则存在一定技术门槛,需要设计者对关系代数等领域有比较深的理解。SQL 解析的结果需要尽量和主流的 ANSI-SQL 一致,这样也能降低公司的推广成本、使用者的学习成本。此外,大数据处理时代的分布式计算场景下,往往一条 SQL 可以解析成多棵语义对等的语法树,但考虑到不同数据结构、底层数据处理的量级、内部的过滤连接等操作的逻辑,这些语法树之间的具体执行效率往往差别很大,SQL 语句不同,底层的执行环境不同,存在的优劣选择也各不相同。

        因此,怎么优化这些语法树的执行路径就是一个非常重要的课题。在这两点上,大数据处理中的批量计算、流计算、交互查询等领域,多少都会存在一些共性问题,当把查询语句背后的关系代数、查询处理和优化等问题封装抽象之后,则有产生一个通用框架的可能。

        如果你是一个数据使用者,可能会面临多种异构数据源需要整合(有传统的关系数据库、搜索引擎如 ES、缓存产品如 MongoDB、分布式计算框架如 Spark 等等),此时同样可能面临跨平台的查询语句分发及执行优化等课题。

        因此 Apache Calcite 应运而生,论文里把它定位为一个完整的查询处理系统,但 Calcite 的设计是非常灵活,实际项目中一般有两种使用方式:1.把 Calcite 当作 lib 库,嵌入到项目中。2.实现一个适配器(Adapter),项目通过读取数据源的适配器与 Calcite 集成。

Apache Calcite 在数据管理(data managerment)数据存储(data storage)开放给各外部计算、存储引擎来实现。因为数据本身的特性,导致上述两部分既多样又复杂,如文件、关系数据库、列数据库、分布式存储等,所以Calcite 专注于上层更通用的Query language、Query Optimizer、Query Execution,使系统复杂性获得有效控制,聚焦于擅长的领域。


什么是关系代数Relational algebra ?

关系代数,是关系型数据库操作的理论基础。关系代数支持 并、差、笛卡尔积、投影和选择等基本运算。

名称数学符号举例说明
select 选择σ、 σC(R)、σlength≥100(Movies)类比SQL的where
projection 投影π、πname,birthdate类比SQL的select
union 并

⋃ 、R ⋃ S

类比SQL的union
intersection 集合交

⋂、R ⋂ S

SQL没有对应的操作符
set-difference 集合差

— 、R — S;S—R

SQL没有对应的操作符
Cartesian-Product 笛卡尔乘积x、RxS类比SQL不含on条件的inner join
rename 重命名ρ、ρS(R)类比SQL的as
natural-Joins 自然连接⋈、R⋈S类比SQLinner join

左外连接:符号⋊表示

右外连接:符号⋉表示

全外连接:符号表示

theta-joins θ连接theta指的是一些随机条件,用代表θ。R⋈CS
assignment 赋值←、temp1←R×S
Division 除法÷、R÷S比如A,B两表。A是记录朋友谁和谁是朋友的表,B是一个名单表。你要查询和B上所有人员都为朋友的人员有哪些就用A除B
谓词、连词谓词中进行比较,使用的是, =,≠,<,>。也可以使用连词and(Λ), or(ν), 和not(¬)将多个谓词合并为一个较大的谓词。


关系代数也是 Calcite 的核心, 任何一个查询都可以表示成由关系运算符组成的树(tree of relational operators)。 在 Calcite 中,它会先将 SQL 转换成关系表达式(relational expression),然后通过规则匹配(rules match)进行相应的优化,优化会有一个成本(cost)模型为参考。

官方论文(原文)
Operators. 
Relational algebra [11] lies at the core of Calcite. In addition to the operators that express the most common data manipulation operations, such as filter, project, join etc., 

Calcite includes additional operators that meet different purposes, e.g., being able to
concisely represent complex operations, or recognize optimization opportunities more efficiently.

For instance, it has become common for OLAP, decision making,and streaming applications to use window definitions to express complex analytic functions such as moving average of a quantity over a time period or number or rows. Thus, Calcite introduces a window operator that encapsulates the window definition, i.e., upper and lower bound, partitioning etc., and the aggregate functions to execute on each window
官方论文(中文机器谷歌翻译版)

操作算⼦

关系代数[11]是 Calcite 的核⼼。除了表达最常⻅的数据操作的操作算⼦,如过滤器、项⽬、连接等,⽅解⽯还包括满⾜不同⽬的的附加算⼦,例如,能够简洁地表⽰复杂的操作,或者更有效地识别优化机会.

例如,OLAP、决策制定和流应⽤程序使⽤窗⼝定义来表达复杂的分析函数已变得很常⻅,例如⼀段时间内数量或数量或行的移动平均值。

因此,Calcite 引⼊了⼀个窗⼝运算符,它封装了窗⼝定义,即上限和下限、分区等,以及在每个窗⼝上执行的聚合函数。

什么是数据库系统的查询优化?

查询优化,主要是围绕着 等价交换 的原则做相应的转换。这部分可以参考【《数据库系统概念(中文第六版)》第13章——查询优化】。关于查询优化理论知识,列出一些个人觉得不错的博客,大家可以参考一下:

数据库查询优化入门: 代数与物理优化基础

数据库查询优化入门: 代数与物理优化基础 - 简书 (jianshu.com)

高级数据库十五:查询优化器(一)_SuPhoebe的博客-CSDN博客_数据库优化器
高级数据库十六:查询优化器(二)_SuPhoebe的博客-CSDN博客_数据库 查询优化器

「 数据库原理 」查询优化(关系代数表达式优化)

4.1.3 关系数据库系统的查询优化(1)

4.1.3 关系数据库系统的查询优化(10) - 51CTO.COM


Calcite架构

calcite架构与传统数据库管理系统有一些相似之处,相比而言, 它将数据存储、数据处理算法和元数据存储这些部分忽略掉了。其好处:对于涉及多种数据源和多种计算引擎的应用而言,Calcite 因为可以兼容多种存储和计算引擎,使得 Calcite 可以提供统一查询服务。
        Calcite 架构中,最核心地方就是 Optimizer优化器,一个 Optimization Engine 包含三个组成部分(The optimization engine primarily consists of three components: rules, metadata providers, and planner engines.)
        Rules:匹配规则,Calcite 内置上百种 Rules 来优化 relational expression,同时也支持自定义 rules;
        Metadata providers:主要是向优化器提供信息,这些信息会有助于指导优化器向着目标(减少整体 cost)进行优化, 信息可以包括行数、table 哪一列是唯一列等,也包括计算 RelNode 树中执行 subexpression cost 的函数。
        Planner engines:主要是进行触发 rules 来达到指定目标,比如像 cost-based optimizer(CBO)的目标是减少cost (Cost 包括处理的数据行数、CPU cost、IO cost 等)

        Calcite架构图如上,虚线表示 Calcite 与外部的相互作用。(1)图中最上面的 JDBC client 表示外部的应用,访问时一般以 SQL的形式输入,通过 JDBC Client 访问 Calcite 内部的 JDBC Server 。(2)接下来 JDBC Server 会把传入的 SQL 语句经过 SQL Parser and Validator 模块做 SQL 的解析和校验。(3)旁边的 Expressions Builder 支持 Calcite 做 SQL 解析和校验的框架对接。(4) Query Optimizer 根据Pluggable Rules对Operator Expressions进行优化,其中会用到Metadata Providers(支持外部自定义元数据)提供的信息进行代价计算等操作。而Operator Expressions 是由关系运算符组成的树(tree of relational operators as its internal representation)在calcite中的表示 ,可以直接通过calcite的SQL Parser解析得到,也可以通过Expressions Builder由Data Processing System中的查询树(a SQL query to a tree,如hive中的AST)转换得到。

摘要
Calcite contains a query parser and validator 
that can translate a SQL query to a tree of relational operators
官方论文(原文)
3	ARCHITECTURE
Calcite contains many of the pieces that comprise a typical database management system. However, it omits some key components, e.g., storage of data, algorithms to process data, and a repository for storing metadata. These omissions are deliberate: it makes Calcite an excellent choice for mediating between applications having one or more data storage locations and using multiple data processing engines. It is also a solid foundation for building bespoke data processing systems.

Figure 1 outlines the main components of Calcite’s architecture. 
Calcite’s optimizer uses a tree of relational operators as its internal representation. The optimization engine primarily consists of three components: rules, metadata providers, and planner engines. We discuss these components in more detail in Section 6. In the figure, the dashed lines represent possible external interactions with the framework. There are different ways to interact with Calcite.

First, Calcite contains a query parser and validator that can translate a SQL query to a tree of relational operators. As Calcite does not contain a storage layer, it provides a mechanism to define table schemas and views in external storage engines via adapters (described in Section 5), so it can be used on top of these engines. 

Second, although Calcite provides optimized SQL support to systems that need such database language support, it also provides optimization support to systems that already have their own language parsing and interpretation:
Some systems support SQL queries, but without or with limited query optimization. 
For example, both Hive and Spark initially offered support for the SQL language, but they did not include an optimizer. 
For such cases, once the query has been optimized, Calcite can translate the relational expression back to SQL. 
This feature allows Calcite to work as a standalone system on top of any data management system with a SQL interface, but no optimizer.

The Calcite architecture is not only tailored towards optimizing SQL queries. It is common that data processing systems choose to use their own parser for their own query language. 
Calcite can help optimize these queries as well. Indeed, Calcite also allows operator trees to be easily constructed by directly instantiating relational operators. One can use the builtin relational expressions builder interface. 

For instance, assume that we want to express the following Apache Pig [41] script using the expression builder:
官方论文(中文机器谷歌翻译版)
3 架构
Calcite 包含许多组成典型数据库管理系统的部分。但是,它省略了⼀些关键组件,例如数据存储、处理数据的算法以及⽤于存储元数据的存储库。这些遗漏是故意的:它使 Calcite成为在具有⼀个或多个数据存储位置并使⽤多个数据处理引擎的应⽤程序之间进行调解的绝佳选择。它也是构建定制数据处理系统的坚实基础。

图 1 概述了 Calcite 架构的主要组件。
Calcite 的优化器使⽤关系运算符树作为其内部表⽰。优化引擎主要由三个组件组成:规则、元数据提供者和规划引擎planner。我们将在第 6 节中更详细地讨论这些组件。在图中,虚线表⽰与框架可能的外部交互。有不同的⽅式与⽅解⽯进行交互。

⾸先,Calcite 包含⼀个查询解析器和验证器,可以将 SQL 查询转换为关系运算符树。由于 Calcite不包含存储层,它提供了⼀种机制来通过适配器(在第 5 节中描述)在外部存储引擎中定义表模式和视图,因此它可以在这些引擎之上使⽤。

其次,虽然 Calcite 为需要此类数据库语⾔⽀持的系统提供了优化的 SQL ⽀持,但它也为已经拥有⾃⼰的语⾔解析和解释的系统提供了优化⽀持:⼀些系统⽀持 SQL 查询,但没有或有有限的查询优化。例如,Hive和 Spark最初都提供对SQL语⾔的⽀持,但它们不包含优化器。
对于这种情况,⼀旦优化了查询,Calcite 就可以将关系表达式转换回SQL。此功能允许 Calcite在任何具有 SQL 接⼝但没有优化器的数据管理系统之上作独⽴系统⼯作。 
• Calcite 架构不仅针对优化 SQL 查询⽽量⾝定制。数据处理系统通常选择将⾃⼰的解析器⽤于⾃⼰的查询语⾔。 

Calcite 也可以帮助优化这些查询。实际上,Calcite 还允许通过直接实例化关系运算符来轻松构建运算符树。可以使⽤内置的关系表达式构建器界⾯。例如,假设我们想使⽤表达式构建器来表达以下 Apache Pig [41] 脚本:

Calcited源码的核心概念

概念解释
Catelog

主要定义SQL语义相关的元数据与命名空间。

SQL parser主要是把SQL转化成AST.
SQL validator通过Catalog来校证AST.
Query optimizer将AST转化成物理执行计划、优化物理执行计划.
SQL generator反向将物理执行计划转化成SQL语句.
概念解释
RelOptRule

描述:transforms an expression into another。对 expression 做等价转换

特点:根据传递给它的 RelOptRuleOperand 来对目标 RelNode 树进行规则匹配,匹配成功后,会再次调用 matches() 方法进行进一步检查。如果 mathes结果为真,则调用 onMatch() 进行转换。

ConverterRule

描述:Abstract base class for a rule which converts from one calling convention to another without changing semantics.

特点:它是 RelOptRule 的子类,专门用来做 数据源之间的转换(Calling convention),ConverterRule 一般会调用对应的 Converter 来完成工作,比如说:JdbcToSparkConverterRule 调用 JdbcToSparkConverter 来完成对 JDBC Table 到 Spark RDD 的转换。

RelNode

描述:relational expression,RelNode 会标识其 input RelNode 信息,这样就构成了一棵 RelNode 树

特点:relational expression,which contains input RelNode。代表了对数据的一个处理操作,常见的操作有 Sort、Join、Project、Filter、Scan 等。它蕴含的是对整个 Relation 的操作,而不是对具体数据的处理逻辑。

Converter

描述:A relational expression implements the interface Converter to indicate that it converts a physical attribute, or RelTrait of a relational expression from one value to another.

特点:用来把一种 RelTrait 转换为另一种 RelTrait 的 RelNode。如 JdbcToSparkConverter 可以把 JDBC 里的 table 转换为 Spark RDD。如果需要在一个 RelNode 中处理来源于异构系统的逻辑表,Calcite 要求先用 Converter 把异构系统的逻辑表转换为同一种 Convention。

RexNode

描述:Row-level expression

特点:行表达式(标量表达式),蕴含的是对一行数据的处理逻辑。每个行表达式都有数据的类型。这是因为在 Valdiation 的过程中,编译器会推导出表达式的结果类型。常见的行表达式包括字面量 RexLiteral, 变量 RexVariable, 函数或操作符调用 RexCall 等。 RexNode 通过 RexBuilder 进行构建。

RelTrait

描述:RelTrait represents the manifestation of a relational expression trait within a trait definition.

特点:用来定义逻辑表的物理相关属性(physical property),三种主要的 trait 类型是:Convention、RelCollation、RelDistribution;

Convention

描述:Calling convention used to repressent a single data source, inputs must be in the same convention

特点:继承自 RelTrait,类型很少,代表一个单一的数据源,一个 relational expression 必须在同一个 convention 中;

RelTraitDef

描述:

特点:主要有三种:ConventionTraitDef:用来代表数据源。 RelCollationTraitDef:用来定义参与排序的字段。 RelDistributionTraitDef:用来定义数据在物理存储上的分布方式(比如:single、hash、range、random 等);

RelOptCluster

描述:An environment for related relational expressions during the optimization of a query.

特点:palnner 运行时的环境,保存上下文信息;

RelOptPlanner

描述:A RelOptPlanner is a query optimizer: it transforms a relational expression into a semantically equivalent relational expression, according to a given set of rules and a cost model.

特点:优化器,Calcite 支持 RBO(Rule-Based Optimizer) 和 CBO(Cost-Based Optimizer)。Calcite 的 RBO (HepPlanner)称为 启发式优化器(heuristic implementation ), 它简单地按 AST 树结构匹配所有已知规则,直到没有规则能够匹配为止;Calcite 的 CBO 称为 火山式优化器(VolcanoPlanner)成本优化器也会匹配并应用规则,当整棵树的成本降低趋于稳定后,优化完成,成本优化器依赖于比较准确的成本估算。RelOptCost 和 Statistic 与成本估算相关;

RelOptCost

描述:defines an interface for optimizer cost in terms of number of rows processed, CPU cost, and I/O cost.

特点:优化器成本模型会依赖。

引用博客园青紫天涯的原话:Calcite 抛出的概念非常多,笔者最开始在看代码时就被这些概念绕得云里雾里,这时候先从代码的细节里跳出来,先把这些概念理清楚、归归类后再去看代码,思路就清晰很多,因此,在介绍 Calcite 整体实现前,先把这些概念梳理一下,需要对这些概念有个基本的理解,相关的概念如下图所示:

 


Calcite 解析SQl的步骤

 Calcite 解析步骤如上图。Calcite解析SQL有以下几步:

SQL 解析--SQL 校验--查询优化--SQL 生成器--数据连接
步骤具体
解析 SQL

Parser. 该步骤Calcite通过Java CC 将SQL解析成未经校验的AST(抽象语法树),在 Calcite 中用 SqlNode 来表示;

语法检查

Validate. 根据数据库的元数据信息meta data进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树 (标准化修改:比如只写join会变成inner join);

其中,为了实现灵活的元数据功能,Calcite 需要支持运行时编译 Java 代码,而默认的 JavaC 太重,需要一个更轻量级的开源的编译器Janino

语义分析

根据 SqlNode 及元信息构建 RelNode 树,也就是 最初版本的逻辑计划(Logical Plan);

Converter. 该步骤主要作用是校证Parser步骤中的AST是否合法,如验证SQL scheme、字段、函数等是否存在; SQL语句是否合法等. 此步完成之后就生成了RelNode树(关于RelNode树, 请参考下文)

逻辑计划优化

优化器的核心, 根据前面生成的逻辑计划按照相应的规则(Rule)进行优化(逻辑计划调整);

Optimize. 该步骤主要的作用优化RelNode树, 并将其转化成物理执行计划。主要涉及SQL规则优化如:基于规则优化(RBO)及基于代价(CBO)优化;

Optimze 这步原则上来说是可选的, 通过Validate后的RelNode树已经可以直接转化物理执行计划,但现代的SQL解析器基本上都包括有这一步,目的是优化SQL执行计划。此步得到的结果为物理执行计划。

物理执行

生成物理计划,物理执行计划执行 (具体执行计划就要看存储引擎如何设计,比较Mysql有Innodb,Hive默认使用HDFS存储)。

Execute,即执行阶段。此阶段主要做的是:将物理执行计划转化成可在特定的平台执行的程序。如Hive与Flink都在在此阶段将物理执行计划CodeGen生成相应的可执行代码。

Calcite 解析SQl的步骤(伪代码版)

(1)解析SQL ,即SQL➡️SqlNode

Calcite 进行 Sql 解析的代码如下:

SqlParser parser = SqlParser.create(sql, SqlParser.Config.DEFAULT);
SqlNode sqlNode = parser.parseStmt();

当 SqlParser 调用 parseStmt() 方法后,其相应逻辑如下: 

package org.apache.calcite.sql.parser;

/**
 * A <code>SqlParser</code> parses a SQL statement.
 * org.apache.calcite.sql.parser.SqlParser
 */
public class SqlParser {
//...省略...
    public SqlNode parseStmt() throws SqlParseException {
      return parseQuery();
    }

//...省略...
    public SqlNode parseQuery() throws SqlParseException {
      try {
        //parser是private final SqlAbstractParserImpl parser;的实现类SqlParserImpl类
        return parser.parseSqlStmtEof();         //解析sql语句。
      } catch (Throwable ex) {
            if (ex instanceof CalciteContextException) {
              final String originalSql = parser.getOriginalSql();
              if (originalSql != null) {
                ((CalciteContextException) ex).setOriginalStatement(originalSql);
              }
      } throw parser.normalizeException(ex);
    }}
}

 SqlParser类中 parser 是 SqlParserImpl 类(SqlParser.Config.DEFAULT 指定的),它就是由 JJ 文件生成的解析类,其处理流程如下,具体解析逻辑还是要看 JJ 文件中的定义。

package org.apache.calcite.sql.parser.impl;

/**
 * SQL parser, generated from Parser.jj by JavaCC.
 * <p>The public wrapper for this parser is {@link SqlParser}.
 * //org.apache.calcite.sql.parser.impl.SqlParserImpl
 */
public class SqlParserImpl extends SqlAbstractParserImpl implements SqlParserImplConstants {
    //...省略...
    public SqlNode parseSqlStmtEof() throws Exception{
        return SqlStmtEof();
    }
    //...省略...
    /**
     * Parses an SQL statement followed by the end-of-file symbol.
     * 解析SQL语句(后面有文件结束符号)
     */
    final public SqlNode SqlStmtEof() throws ParseException {
      SqlNode stmt;
      stmt = SqlStmt();
      jj_consume_token(0);
          {if (true) return stmt;}
      throw new Error("Missing return statement in function");
    }
    //...省略...
    //解析 SQL statement
    final public SqlNode SqlStmt() throws ParseException {
      SqlNode stmt;
      if (jj_2_34(2)) {
        stmt = SqlSetOption(Span.of(), null);
      } else if (jj_2_35(2)) {
        stmt = SqlAlter();
      } else if (jj_2_36(2)) {
        stmt = OrderedQueryOrExpr(ExprContext.ACCEPT_QUERY);
      } else if (jj_2_37(2)) {
        stmt = SqlExplain();
      } else if (jj_2_38(2)) {
        stmt = SqlDescribe();
      } else if (jj_2_39(2)) {
        stmt = SqlInsert();
      } else if (jj_2_40(2)) {
        stmt = SqlDelete();
      } else if (jj_2_41(2)) {
        stmt = SqlUpdate();
      } else if (jj_2_42(2)) {
        stmt = SqlMerge();
      } else if (jj_2_43(2)) {
        stmt = SqlProcedureCall();
      } else {
        jj_consume_token(-1);
        throw new ParseException();
      }
          {if (true) return stmt;}
      throw new Error("Missing return statement in function");
    }
}

Calcite 使用 JavaCC 做 SQL 解析,JavaCC 根据 Calcite 中定义的 Parser.jj 文件,生成一系列的 java 代码,生成的 Java 代码会把 SQL 转换成 AST 的数据结构(这里是 SqlNode 类型)。

与 Javacc 相似的工具还有 ANTLR,JavaCC 中的 jj 文件也跟 ANTLR 中的 G4文件类似,Apache Spark 中使用这个工具做类似的事情。


Javacc

关于 Javacc 内容可以参考下面这几篇文章,这里就不再详细展开,通过下面文章的例子把 JavaCC 的语法了解一下,这样我们也可以设计一个 DSL(Doomain Specific Language)。

JavaCC 研究与应用( 8000字 心得 源程序); https://www.cnblogs.com/Gavin_Liu/archive/2009/03/07/1405029.html
JavaCC、解析树和 XQuery 语法,第 1 部分;https://www.ibm.com/developerworks/cn/xml/x-javacc/part1/index.html
JavaCC、解析树和 XQuery 语法,第 2 部分;https://www.ibm.com/developerworks/cn/xml/x-javacc/part2/index.html
编译原理之Javacc使用;https://www.yangguo.info/2014/12/13/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86-Javacc%E4%BD%BF%E7%94%A8/

回到 Calcite,Javacc 这里要实现一个 SQL Parser,它有以下两个功能都需要在 jj 文件中定义。

  1. 设计词法和语义,定义 SQL 中具体的元素;
  2. 实现词法分析器(Lexer)和语法分析器(Parser),完成对 SQL 的解析,完成相应的转换。 

 实战:SQL解析成为SqlNode

package org.xiaoke.queryDemo;

import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.impl.AbstractTable;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.type.BasicSqlType;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.tools.Frameworks;


public class Xiaoke {
    
    public static void main(String[] args) throws SqlParseException {
        String sql = new String(
                "select u.id as user_id, u.name as user_name, j.company as user_company, u.age as user_age\n" +
                        "        from users u                     " +
                        "        join jobs j                      " +
                        "           on u.name=j.name            \n" +
                        "        where u.age > 30 and j.id>10   \n" +
                        "        order by user_id");

        Xiaoke xiaoke = new Xiaoke();
        xiaoke.xxxx();
        SqlParser parser = SqlParser.create(sql, SqlParser.Config.DEFAULT);
        SqlNode sqlNode = parser.parseStmt();
    }
    
    private void xxxx(){
    /*
         select u.id as user_id, u.name as user_name, j.company as user_company, u.age as user_age
         from users u join jobs j on u.name=j.name
         where u.age > 30 and j.id>10
         order by user_id
         这里有两张表,其表各个字段及类型定义如下:
    */
        SchemaPlus rootSchema = Frameworks.createRootSchema(true);
        rootSchema.add("USERS", new AbstractTable() { //note: add a table
            @Override
            public RelDataType getRowType(final RelDataTypeFactory typeFactory) {
                RelDataTypeFactory.Builder builder = typeFactory.builder();

                builder.add("ID", new BasicSqlType(new RelDataTypeSystemImpl() {}, SqlTypeName.INTEGER));
                builder.add("NAME", new BasicSqlType(new RelDataTypeSystemImpl() {}, SqlTypeName.CHAR));
                builder.add("AGE", new BasicSqlType(new RelDataTypeSystemImpl() {}, SqlTypeName.INTEGER));
                return builder.build();
            }
        });

        rootSchema.add("JOBS", new AbstractTable() {
            @Override
            public RelDataType getRowType(final RelDataTypeFactory typeFactory) {
                RelDataTypeFactory.Builder builder = typeFactory.builder();

                builder.add("ID", new BasicSqlType(new RelDataTypeSystemImpl() {}, SqlTypeName.INTEGER));
                builder.add("NAME", new BasicSqlType(new RelDataTypeSystemImpl() {}, SqlTypeName.CHAR));
                builder.add("COMPANY", new BasicSqlType(new RelDataTypeSystemImpl() {}, SqlTypeName.CHAR));
                return builder.build();
            }
        });
    }

}

示例中 SQL 经过前面的解析之后,会生成一个 SqlNode(SqlOrder 类型,因案例SQL使用了order by),DEBUG 后的 SqlOrder 对象如下图所示。



(2)语法检查,即SqlNode➡️SqlNode


经过上一步,会生成一个 SqlNode 对象,它是一个未经验证的抽象语法树,下面就进入了一个语法检查阶段, 语法检查前需要知道元数据信息,这个检查会包括 表名、字段名、函数名、数据类型的检查。
     
Calcite 本身是不管理和存储元数据的,在检查之前, 需要先把元信息注册到 Calcite 中,一般的操作方法是实现 SchemaFactory, 由它去创建相应的 Schema,在 Schema 中可以注册相应的元数据信息。
     
SqlValidatorImpl 检查过程
rewrite expression, 将其标准化,便于后面的逻辑计划优化;
注册这个 relational expression 的 scopes 和 namespaces (这两个对象代表了其元信息);
进行相应的验证,这里会依赖第二步注册的 scopes 和 namespaces 信息。
     

(3)语义分析,即SqlNode➡️RelNode/RexNode


主要步骤:
初始化 RexBuilder;
初始化 RelOptPlanner;
初始化 RelOptCluster;
初始化 SqlToRelConverter;
进行转换;
     
转换部分
SqlToRelConverter 中的 convertQuery() 将 SqlNode 转换为 RelRoot
真正实现部分:convertQueryRecursive
     

(4)逻辑计划优化,即RelNode➡️RelNode


提供两种 planner:HepPlanner 和 VolcanoPlanner

名称解释
HepPlanner

(1)HepPlanner is a heuristic optimizer similar to Spark’s optimizer

与 spark 的优化器相似,HepPlanner 是一个 heuristic (启发式的;探试的,探索的)优化器;

(2)Applies all matching rules until none can be applied.

将会匹配所有的 rules 直到某个 rule 被满足;

(3)Heuristic optimization is faster than cost- based optimization

它比 CBO 更快;

(4)Risk of infinite recursion if rules make opposing(反作用的) changes to the plan.

如果没有每次都不匹配规则,可能会有无限递归风险;

VolcanoPlanner

(1)VolcanoPlanner is a cost-based optimizer

VolcanoPlanner是一个CBO优化器;

(2)Applies matching rules iteratively, selecting the plan with the cheapest cost on each iteration.

迭代地应用 rules,直到找到cost最小的plan;

(3)Costs are provided by relational expressions.Not all possible plans can be computed.

不会计算所有可能的计划

(4)Stops optimization when the cost does not significantly improve through a determinable number of iterations.

根据已知的情况, 如果下面的迭代不能带来提升时,这些计划将会停止优化(类似与机器学习中损失函数,只不过early_stop为1个epoch);


简单的优化 (过滤条件下压,也称为谓词下推)
关于filter 操作下压,在 Calcite 中已经有相应的 Rule 实现,就是 FilterJoinRule.FilterIntoJoinRule.FILTER_ON_JOIN
这里使用 HepPlanner 作为示例的 planer,并注册 FilterIntoJoinRule 规则进行相应的优化(这里可以看到where u.age>10 and j.id>0被下推到扫表操作上面,也就是说SQL引擎扫描一遍表时如果发现表中某行满足过滤条件则拿取出来,否则跳过)

4. Calcite相关组件

Calcite主要有以下概念:

  • Catelog: 主要定义SQL语义相关的元数据与命名空间。
  • SQL parser: 主要是把SQL转化成AST.
  • SQL validator: 通过Catalog来校证AST.
  • Query optimizer: 将AST转化成物理执行计划、优化物理执行计划.
  • SQL generator: 反向将物理执行计划转化成SQL语句.

4.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用于计表计算表的代价。

一句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转化成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>
....

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转化为:

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)

那接下来我们可能有一个疑问,在什么情况下可以做类似下推、上推操作,又是根据什么原则进行的呢?如下图所示

不同的JOIN顺序

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,有关此算法的具体内容可以参考原码

5. 如何使用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;
    }

}

详细可以代码在这里


Calcite代码实战

(1)maven的pom.xml

<dependency>
    <groupId>org.apache.calcite</groupId>
    <artifactId>calcite-core</artifactId>
    <version>1.19.0</version>
</dependency>

截至20220503的最新版本存在不兼容的情况,可以考虑上面的版本
<dependency>
    <groupId>org.apache.calcite</groupId>
    <artifactId>calcite-core</artifactId>
    <version>1.30.0</version>
</dependency>

 (2)入门级demo

package org.myApache.queryDemo;

import org.apache.calcite.config.Lex;
import org.apache.calcite.sql.*;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.SqlParserPos;

/**
 * 参考文章:https://www.freesion.com/article/7723688675/
 *         https://liebing.org.cn/2022/05/05/2022/apache_calcite_sql_parser/
 *  Flink SQL中使用Calcite作为sql语法解析、校验、优化工具,本篇是实操篇,介绍一下calcite做sql语法解析使用方式。
 *  注:目前20220504, Calcite 的官方最新版本是 v1.30,Flink 1.12 使用的是 Calcite v1.26,本文的内容基于 Calcite 1.20 编写,但所有核心内容均不受版本影响。
 */
public class ToMysqlDemo01 {

    public static void main(String[] args) {
        String Sql1 = new String("select id,name,age FROM student where age < 20");
        String Sql2 = new String("select sum(amount) FROM orders");
        String Sql3 = new String("select cast(amount as CHAR) FROM orders");

        SqlParser.Config config = SqlParser.configBuilder()
                .setLex(Lex.MYSQL) //使用mysql 语法
                .build();

        ToMysqlDemo01 toMysql = new ToMysqlDemo01();
        toMysql.parserEasySelect(config,Sql1);
        toMysql.parserSelectContainSum(config,Sql2);
        toMysql.parserSelectContainCast(config,Sql3);
    }


    /**
     * 解析简单的select语句
     * @param config
     * @param Sql
     *
     ***
     *  一个select语句包含from部分、where部分、select部分等,每一部分都表示一个SqlNode。
     *  SELECT ca, cb, cc FROM t WHERE ca = 10;
     *  为了有更直观的感受, 我们配合以上SQL语句来讲解SqlNode各个子类所代表的含义.
     *      SqlIdentifier代表标识符,例如表名称、字段名;SqlLiteral表示字面常量,一些具体的数字、字符。 上述SELECT语句中ca, cb, cc以及t在解析树中都是一个SqlIdentifier实例.
     *      SqlLiteral 代表常量, 上述SELECT语句中10在解析树中就是一个SqlLiteral实例, 它的具体实现类是SqlNumericLiteral, 表示数字常量.
     *      SqlNodeList 表示SqlNode列表, 上述SELECT语句中ca, cb, cc会共同组成一个SqlNodeList实例.
     *      SqlCall 表示对SqlOperator的调用. (SqlOperator可以用来描述任何语法结构, 所以实际上SQL解析树中的每个非叶节点都是某种SqlCall).
     *              上述整个SELECT语句就是一个SqlCall实例, 它的具体实现类是SqlSelect.
     *      SqlDataTypeSpec 表示解析树中的SQL数据类型, 上述CREATE语句中的INT, DOUBLE, VARCHAR在解析树中都是一个SqlDataTypeSpec实例.
     *      SqlIntervalQualifier 表示时间间隔限定符, 比如SQL中的INTERVAL '1:23:45.678' HOUR TO SECOND在解析树中就是一个SqlIntervalQualifier实例.
     *      SqlDynamicParam 表示SQL语句中的动态参数标记.
     *
     ***
     *  在SqlNode的子类中, SqlLiteral和SqlCall有各自的实现类.
     *  我们先分析简单的SqlLiteral及其实现类, 它的类继承结构如下图所示. 其实每种实现类就代表了一种特定的常量类型, 比如字符串, 数字, 时间, 时间间隔. 根据类名即可望文生义, 这里不再过多介绍.
     *
     *  由于SqlCall的实现类较多, 这里我们仅选择部分有代表性的实现类进行详细介绍.
     *      SqlSelect  表示整个SELECT语句的解析结果, 内部有from, where, group by等成员变量保存对应字句内的解析结果.
     *      SqlOrderBy 表示带有ORDER BY的SELECT语句的解析结果.
     *      SqlInsert和SqlDelete分别表示INSERT和DELETE语句的解析结果.
     *      SqlJoin 表示JOIN子句的解析结果.
     *      SqlBasicCall 表示一个基本的计算单元, 持有操作符和操作数, 如WHERE子句中的一个谓词表达式就会被解析为SqlBasicCall.
     *      SqlDdl 是DDL语句解析结果的基类. 以CREATE TABLE语句为例, 它就会被解析成SqlCreateTable实例.
     ***
     *  SqlKind是一个枚举类型,包含SQL中的各种部分的定义。
     *
     ***
     *  上文说到SqlCall其实包含对SqlOperator的调用, 因此我们有必要进一步看一下SqlOperator的实现.
     *  SqlOperator其实可以表达SQL语句中的任意运算, 它包括函数, 操作符(如=)和语法结构(如case语句).
     *  SqlOperator可以表示查询级表达式(如SqlSelectOperator或行级表达式(如SqlBinaryOperator).
     *  由于SqlOperator的实现类较多, 这里我们同样仅挑选几个有代表性的类进行说明.
     *      SqlFunction 表示SQL语句中的函数调用, 如SqlCastFunction表示cast函数, 在解析阶段所有自定义函数都会被表示为SqlUnresolvedFunction,在验证阶段才会转化为对应的SqlUserDefinedFunction.
     *      SqlSelectOperator 表示整个SELECT查询语句.
     *      SqlBinaryOperator 表示二元运算, 如WHERE子句中的=运算.
     */
    private void parserEasySelect (SqlParser.Config config,String Sql){
        //SqlParser 语法解析器
        SqlParser sqlParser = SqlParser.create(Sql, config);
        SqlNode sqlNode = null;
        try {
            sqlNode = sqlParser.parseStmt();
        } catch (SqlParseException e) {
            throw new RuntimeException("", e);
        }

        if(SqlKind.SELECT.equals(sqlNode.getKind())){

            SqlSelect sqlSelect = (SqlSelect) sqlNode;
            SqlNode from = sqlSelect.getFrom();
            SqlNode where = sqlSelect.getWhere();
            SqlNodeList selectList = sqlSelect.getSelectList();
            //标识符
            if(SqlKind.IDENTIFIER.equals(from.getKind())){
                System.out.println("the table is '"+from.toString()+"'.");
            }

            if(SqlKind.LESS_THAN.equals(where.getKind())){
                SqlBasicCall sqlBasicCall = (SqlBasicCall)where;
                for(SqlNode sqlNode1: sqlBasicCall.operands){
                    if(SqlKind.LITERAL.equals(sqlNode1.getKind())){
                        System.out.println("the LITERAL字面常量 is '"+sqlNode1.toString()+"'.");
                    }
                }
            }

            selectList.getList().forEach(x->{
                if(SqlKind.IDENTIFIER.equals(x.getKind())){
                    System.out.println("the IDENTIFIER标识符-字段名 is '"+x.toString()+"'.");
                }}
            );
        }
    }

    /**
     * 解析含有sum()的select语句
     * @param config
     * @param Sql
     *
     */
    private void parserSelectContainSum(SqlParser.Config config, String Sql) {
        //SqlParser 语法解析器
        SqlParser sqlParser = SqlParser.create(Sql, config);
        SqlNode sqlNode = null;
        try {
            sqlNode = sqlParser.parseStmt();
        } catch (SqlParseException e) {
            throw new RuntimeException("", e);
        }

        if(SqlKind.SELECT.equals(sqlNode.getKind())){

            SqlSelect sqlSelect = (SqlSelect) sqlNode;
            SqlNode from = sqlSelect.getFrom();
            SqlNodeList selectList = sqlSelect.getSelectList();
            //标识符
            if(SqlKind.IDENTIFIER.equals(from.getKind())){
                System.out.println("\n"+"the table is '"+from.toString()+"'.");
            }

            //    SqlBasicCall对比SqlSelect/SqlDelete而言,可以理解为表示的是一些基本的、简单的调用,例如聚合函数AggregateFunctions、比较函数等,接下来看一下其如何解析sum操作:
            //    select sum(amount) FROM orders //解析的sql //解析select部分
            //   其内部主要就是operands,也是SqlNode节点,但是都是一些基本的SqlNode,例如SqlIdentifier、SqlLiteral。
            selectList.getList().forEach(
                    x->{if(SqlKind.SUM.equals(x.getKind())){
                        SqlBasicCall sqlBasicCall = (SqlBasicCall)x;
                        System.out.println(sqlBasicCall.operands[0]);
                    }}
            );
        }
    }


    /**
     * 解析含有cast()的select语句
     * @param config
     * @param Sql
     *
     */
    private void parserSelectContainCast(SqlParser.Config config, String Sql) {
        //SqlParser 语法解析器
        SqlParser sqlParser = SqlParser.create(Sql, config);
        SqlNode sqlNode = null;
        try {
            sqlNode = sqlParser.parseStmt();
        } catch (SqlParseException e) {
            throw new RuntimeException("", e);
        }

        if(SqlKind.SELECT.equals(sqlNode.getKind())){

            SqlSelect sqlSelect = (SqlSelect) sqlNode;
            SqlNode from = sqlSelect.getFrom();
            SqlNodeList selectList = sqlSelect.getSelectList();
            //标识符
            if(SqlKind.IDENTIFIER.equals(from.getKind())){
                System.out.println("\n"+"the table is '"+from.toString()+"'.");
            }

            //    SqlSelect/SqlDelete/SqlBasicCall 都称之为SqlCall,差别是SqlSelect是复杂的SqlCall,内部可以包含其他节点,而SqlBasicCall表示简单的SqlCall。
            //    另外两种SqlNode:SqlDataTypeSpec与SqlNodeList。
            //    (1)SqlDataTypeSpec代表数据类型节点,例如CHAR/VARCHAR/DOUBLE,
            //    (2)SqlNodeList表示包含多个同级别的SqlNode,在上面select中已经展示过,看下SqlDataTypeSpec使用实例:
            //    select cast(amount as CHAR) FROM orders  //解析的sql
            //   其内部主要就是operands,也是SqlNode节点,但是都是一些基本的SqlNode,例如SqlIdentifier、SqlLiteral。
            selectList.getList().forEach(
                    x->{if(SqlKind.CAST.equals(x.getKind())){
                            SqlBasicCall sqlBasicCall = (SqlBasicCall)x;
                            System.out.println(sqlBasicCall.operands[0]); //amount
                            SqlDataTypeSpec charType = (SqlDataTypeSpec)sqlBasicCall.operands[1];
                            System.out.println(charType.getTypeName()); //CHAR
                        }}
            );

            //    另外一种节点SqlOperator,可以代表函数、运算符、语法(select)结构,
            //    例如sum解析为SqlAggFunction、select解析为SqlSelectOperator、as解析为SqlAsOperator。
            //    SqlOperator是被嵌入在SqlNode中,作为其属性,通过SqlOperator的createCall方法可以创建对应的SqlNode
            //    SqlParsePos表示对应解析的节点在sql位置,起止行与起止列。
            SqlOperator operator = new SqlAsOperator();
            SqlParserPos sqlParserPos = new SqlParserPos(1, 1);
            SqlIdentifier name = new SqlIdentifier("orders", null, sqlParserPos);
            SqlIdentifier alias = new SqlIdentifier("o", null, sqlParserPos);
            SqlNode[] sqlNodes = new SqlNode[2];
            sqlNodes[0] = name;
            sqlNodes[1] = alias;
            SqlBasicCall sqlBasicCall = (SqlBasicCall)operator.createCall(sqlParserPos,sqlNodes);
            System.out.println(sqlBasicCall); //得到的就是 Order as o
        }
    }
    //    以上介绍了一下calcite解析sql的简单使用方式,我们可以使用Calcite来做血缘分析、flink sql维表关联等。
}
 你可以将SQL 转换成关系代数,或者通过Calcite 提供的API 直接创建它。
比如下面这段SQL 查询:

SELECT deptno, count(*) AS c, sum(sal) AS s
FROM emp
GROUP BY deptno
HAVING count(*) > 10

可以表达成如下的关系表达式语法树:

LogicalFilter(condition=[>($1, 10)])
  LogicalAggregate(group=[{7}], C=[COUNT()], S=[SUM($5)])
    LogicalTableScan(table=[[scott, EMP]])

6. Calcite 其它方面

Calcite的功能远不止以上介绍,除了标准SQL的,还支持以下内容:

  • 对流相对概念支持,如在SQL层面支持Window概念,如Session Window, Hopping Window等。
  • 支持物化视图等复杂概念。
  • 独立于编程语言和数据源,可以支持不同的前端和后端。

7. 总结

以上内容主要介绍上Calcite相关概念并通过相例子说明了Calcite使用方法, 希望通过上述内容,读者能对Calcite有初步的了解。

由于笔者使用和探索Calcite时间也不长,以上内容难免有错误与不准确之处,还望各位读者不吝指正,相互学习。

参考文献与网址: 

        1.https://www.cnblogs.com/wcgstudy/p/11795886.html
        2.https://www.infoq.cn/article/new-big-data-hadoop-query-engine-apache-calcite
        3.https://www.jianshu.com/p/2dfbd71b7f0f
        4.https://www.slideshare.net/JordanHalterman/introduction-to-apache-calcite
        5.http://hbasefly.com/2017/05/04/bigdata%EF%BC%8Dcbo/

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值