Apache Calcite的解析与优化

Apache Calcite的解析与优化

背景

数据库从RDBMS到NoSQL,再到NewSQL的发展趋势,足以体现SQL在数据库中的重要地位。SQL 作为一项图灵奖级别的发明,给用户确实带来了很多便捷的体验,用户不需要过多关心数据库底层的实现原理,只需要声明式的用SQL来表达业务逻辑,至于执行优化,数据处理都交给数据库底层引擎来实现,极大的降低了使用门槛。SQL 作为一种可以被非相关技术人员快速入手的编程语言,具有以下优点:

  • 声明式:用户只需要表达我想要什么,至于怎么计算那是系统的事情,用户不用关心。
  • 自动调优:查询优化器可以为用户的 SQL 生成最有的执行计划。用户不需要了解它,就能自动享受优化器带来的性能提升。
  • 易于理解:很多不同行业不同领域的人都懂 SQL,SQL 的学习门槛很低,用 SQL 作为跨团队的开发语言可以很大地提高效率。
  • 稳定:SQL 是一个拥有几十年历史的语言,是一个非常稳定的语言,很少有变动。所以当我们升级引擎的版本时,甚至替换成另一个引擎,都可以做到兼容地、平滑地升级。
  • 批流统一:SQL 可以做到 API 层的流与批统一。

SQL的引入主要依赖了关系代数这一工具,使得计算机理解和处理查询的语义可以方便转换。SQL的运行逻辑也主要是将其转换成关系代数再进行优化执行。但是对于实现同一个业务逻辑,关系代数的表现形式可能是多样的。选择不一样的CASE,会有不一样的执行性能,对于用户来讲,尤其是OLAP用户,肯定希望数据库能够快速执行SQL并返回想要的结果,因此对于SQL的优化也就显得非常必要。SQL 优化的本质也是对关系代数的优化。

对优化器的研究从上世纪七十年代既已开始,到如今已经发展了数十年,其中有很多里程碑式的进展,例如Volcano/Cascades。在近些年新出现的一些数据库或计算引擎中,例如TiDB、CockroachDB、GreenPlum、Calcite等,也已经开始探索和尝试Cascades技术。

Calcite概述

Calcite 之前的名称叫做 optiq ,optiq 起初在 Hive 项目中,为 Hive 提供基于成本模型的优化,即 CBO(Cost Based Optimizatio)。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 (一种方案适应所有需求场景)",希望能为不同计算平台和数据源提供统一的查询引擎计算平台和数据源提供统一的查询引擎,Calcite具有flexible(灵活), embeddable(可插拔), and extensible(可扩展)三大特点,它的 SQL Parser 层、Optimizer 层等都可以单独使用,这也是 Calcite 受总多开源框架欢迎的原因之一。

在这里插入图片描述

关系代数

关系代数是关系型数据库操作的理论基础,关系代数支持并、差、笛卡尔积、投影和选择等基本运算。关系代数也是 Calcite 的核心,任何一个查询都可以表示成由关系运算符组成的树。在 Calcite 中,它会先将 SQL 转换成关系表达式(relational expression),然后通过规则匹配(rules match)进行相应的优化,优化会有一个成本(cost)模型为参考。

关系代数简单总结如下:

名称英文符号说明
选择selectσ类似于 SQL 中的 where
投影projectΠ类似于 SQL 中的 select
union类似于 SQL 中的 union
集合差set-difference-SQL中没有对应的操作符
笛卡儿积cartesian-product×类似于 SQL 中不带 on 条件的 inner join
重命名renameρ类似于 SQL 中的 as
集合交intersectionSQL中没有对应的操作符
自然连接natural join类似于 SQL 中的 inner join
赋值assignment

解析

parse: SqlText => SqlNode

Calcite的parse模块是基于javacc实现的。javacc是一个词法分析生成器语法分析生成器。词法分析器于将输入字符流解析成一个一个的token,以下面这段SQL语句为例:

示例1

SELECT id, CAST(score AS INT), 'hello' FROM T WHERE id < 10

它会被分成下面这样一组token:

SELECT id , CAST ( score AS INT ) , ' hello ' FROM T WHERE id < 10

接下来语法分析器会以词法分析器解析出来的token序列作为输入来进行语法分析。分析过程使用递归下降语法解析,LL(k)。其中,第一个L表示从左到右扫描输入;第二个L表示每次都进行最左推导(在推导语法树的过程中每次都替换句型中最左的非终结符为终结符。类似还有最右推导);k表示的是每次向前探索(lookahead)k个终结符。分析所依赖的的词法法则定义在一个parser.jj文件中。

在这里插入图片描述

在经过词法分析和语法分析后一段SQL语句会被解析成一颗抽象语法树(Abstract Syntax Tree,AST),树的节点类型在Calcite中以SqlNode来表示,不同节点以不同子类型的SqlNode来表示。同样以上面的SQL为例,在这段SQL中,

  1. id, score, T 等为SqlIdentifier,表示一个字段名或表名的标识符
  2. select和cast()为SqlCall,表示一个行为或动作,其中cast()为一个SqlBasicCall,表示一个函数调用,具体调用的是什么函数,由其内部的SqlOperator决定,比如这里是一个二元操作符“<”,对应SqlBinaryOperator,operator的名字是“<”,类别是SqlKind.LESS_THAN
  3. int 为SqlDataTypeSpec,表示一个类型定义
  4. 'hello’和10为SqlLiteral,表示一个常量

在Calcite中,所有的操作都是一个SqlCall, 如查询是一个SqlSelect, 删除是一个SqlDelete等,它们都是SqlCall的子类型。select的查询条件等为SqlCall中的参数。示例1的SQL语句最终生成的语法树形式如下:

在这里插入图片描述

如果把示例1中的直接从一个表查询数据,改为从两张表的关联结果中查询数据,例如:

示例2

SELECT id, CAST(score AS INT), 'hello' FROM T1 LEFT OUTER JOIN T2 ON T1.id = T2.id WHERE T1.id < 10

则相应的AST形式如下:
在这里插入图片描述

其中只有FROM子树部分由原来的SqlIdentifier节点变成了一棵SqlJoin子树,其他部分与示例1相同所以在图中省略了。

validate: SqlNode => SqlNode

校验(validate)阶段是对经过parser解析出的AST进行有效性验证,验证的方面主要包括以下两方面:

  1. 表名、字段名、函数名是否正确,如在某个查询的字段在当前SQL位置上是否存在或有歧义(当前可见的多个数据源中同时存在该名称的字段)
  2. 特定类型操作自身的合法性,如group by聚合中的聚合函数是否存在嵌套调用,使用AS重命名时新名字是否是x.y的形式等

针对上面的第一种情况,在校验过程中首先需要明确两个最重要的概念:NameSpace和Scope。NameSpace代表一个逻辑上的数据源,可以是一张表,也可以是一个子查询,而Scope则代表了在SQL的某个位置,表和字段的可见范围。那么从概念中可以看出,在某个SQL位置上,某个字段所对应的scope可能包含多个namespace。在validate阶段解析出来的scope和namespace信息会被保存下来,在后面转换成逻辑执行计划的时候还会用到。

下面通过一个例子来看一下到底什么是namespace和scope。
示例3

SELECT expr1
FROM
  t1,
  t2,
  (SELECT expr2 FROM t3) AS q3
WHERE c1 IN (SELECT expr3 FROM t4)
ORDER BY expr4

在上面这样一段SQL语句中包含四个namespace:

t1
t2
(SELECT expr2 FROM t3) AS q3
(SELECT expr3 FROM t4)

那么对于SQL中的不同表达式,根据它们所在的位置,它们所对应的scope如下:

expr1 数据来源于 t1, t2, q3
expr2 数据来源于 t3
expr3 数据来源于 t1, t2, t4
expr4 数据来源于 t1, t2, q3 加上(取决于方言)SELECT子句中定义的任何别名

那么在校验第一种情况的时候,整个校验过程的核心就在于为不同的SqlNode节点生成其对应的namespace和scope,然后对该SqlNode涉及的字段和namespace与scope的对应关系进行校验。

对于第二种情况的校验,则需要根据具体的节点类型分别实现了。

rel: SqlNode => RelNode + RexNode

RelNode代表了对数据的一个处理操作,常见的操作有 Sort、Join、Project、Filter、Scan 等。它蕴含的是对整个 Relation 的操作,而不是对具体数据的处理逻辑。rel阶段是将由SqlNode组成的一棵抽象语法树转化为一棵由RelNodeRexNode组成的关系代数树,或者称为执行计划RelNode表示关系表达式,如投影(Project),即SELECT,和连接(JOIN)等;RexNode表示行表达式,如示例中的CAST(score AS INT)T1.id < 10。以示例2的语法树为例,在经过rel阶段转换后会生成下图所示的执行计划:

在这里插入图片描述

可以看到转换过程本身其实就是一个对树的结构进行调整和规范的过程。

基于规则的优化-RBO

RBO:Rule-Based Optimization也即“基于规则的优化器”,通常也称为启发式优化器,即基于传统经验定义一系列规则,来优化执行计划。RBO一大特点就是对数据不敏感。通常是基于一些必然会带来优化的规则来实现,一些常见的优化规则,例如谓词下推、投影下推、常量折叠。对于具体规则来说,有些规则一定能带来收益,减小查询的代价。来看下面这个例子:

SELECT pv.siteId, user.nickame
FROM pv JOIN user
ON pv.siteId = user.siteId AND pv.userId = user.id
WHERE pv.siteId = 123;

这是一个典型的包含SCAN、JOIN、FILTER、PROJECT算子的SQL,将 SQL 表示成关系代数,可能是如下形式:

在这里插入图片描述

谓词下推

通过提前过滤数据来减少查询的执行时间

在这里插入图片描述

投影下推

通过只读取特定列,避免读取不相关的数据来减少 IO 损耗、增加执行性能

在这里插入图片描述

以上这些规则对于有些通用场景一定能带来收益,减小查询的代价,但在实际的过程中。有些规则未必一定能带来收益,甚至会增加查询的代价。因此RBO在以下问题上束手无策:

  • 多表Join的选择:比如一张表数据量在小于某值时选择使用brodcast join,另一些情况选择HashJoin或SortMergeJoin

这种情况需要依赖每张表的数据量和大小等信息,因此出现了基于代价的CBO优化模型

基于代价的优化-CBO

CBO:Cost-Based Optimization也即“基于代价的优化器”,该优化器通过根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能“执行计划”的“代价”,即COST,从中选用COST最低的执行方案,作为实际运行方案。

成本最优假设利用了贪心算法的思想,在计算的过程中, 如果一个方案是由几个局部区域组合而成,那么在计算总成本时, 我们只考虑每个局部目前已知的最优方案和成本即可。

实现CBO的模型目前主要有两种,一种是Volcano模型,它的思想是枚举出一个执行计划所有可能的等价转换结果,分别计算出这些结果的代价,然后找出其中代价最小的计划。但是很显然,想要枚举出所有可能的等价转换结果并计算出相应的cost本身就很复杂,并且即使实现了这样的枚举,想要遍历这个巨大的搜索空间来选择最小cost的plan也需要巨大的计算能力的支持,在计算资源有限的情况下这一步本身可能就会耗费大量的计算时间,从而丧失了优化本身的意义。

另外一种是Cascades模型,Calcite的volcano planner就是基于这种模型实现的。Calcite基于Cascades模型的设计思路如下:

  1. 代价最优假设
    首先假定对于一个执行计划的全局最优方案来说,其各个局部组成也是最优的。这一假设实际上是利用了贪心算法的思想。即对于一个节点A和它的两个子节点B、C,它们的代价间存在如下关系:
    Cost(A)∼Cost(B)+Cost©
    基于这一假设,在计算全局最优代价时,只需要依次计算局部最优代价即可。

  2. 动态规划与等价集合
    基于代价最优假设,calcite的volcano planner就不需要枚举并遍历所有可能的等价转换结果空间了,而是首先记录所有可能的规则匹配,然后根据规则的重要性(importance)从高到底依次选取规则执行,利用动态规划的思想,在每次应用规则产生一个等价转换后,用一个等价集合来保存结果,同时记录下等价集合中的最优代价和相应的最优执行计划(即代价最小的执行计划),下一次则基于已有的最优计划和最优代价继续执行优化。这种方法最终通常会得到一个次优的执行计划,但是由于不需要搜索庞大的等价集合空间,执行效率上会更高。

优化的本质其实就是搜索最优代价树的问题。优化器可以分解成三部分:

  • statistics:维护统计信息,用于代价评估
  • cost model:基于统计信息,对具体的执行计划评估代价
  • plan enumeration:对可能的执行计划进行搜索,对其代价进行评估,选择最优执行计划

自底向上

System R选择了自底向上的方式,在搜索过程中,如果是纯粹地枚举所有可能的组合,则搜索空间会非常大。因此通常会对Join Tree的形状进行限制,也会在搜索过程中进行一定的剪枝。

在这里插入图片描述

例如这里的两种典型的Join Tree,Left-deep和Bushy-Join。相比于Bushy-Tree的(2n-2)!/(n-1)!的复杂度,Left-Deep只有n!,搜索空间小了很多。

Interesting Orderd表示上层对下层的输出结果的顺序感兴趣。两表Join的最优解,未必能得到三表Join的最优解,例如两表用了HashJoin,那么输出的结果会是无序的;相比之下,如果用MergeJoin,两表Join可能不是代价最小的, 但是在三表Join时,就可以利用其有序性,对上层的Join进行优化。

这里其实引出一个问题,自底向上的搜索过程中,下层无法知道上层需要的顺序,即便保留所有的Order,也未必能得到最优解,因此引出下面自顶向下的搜索方式。

自顶向下

Volcano/Cascades Optimizer采取了自顶向下的计算方法,在计算开始, 每棵子树先按照原先的样子计算成本并作为初始结果。在不断应用规则的过程中,如果出现一种新的结构被加入到当前的等价集合中, 且这种等价集合具有更优的成本,这时需要向上冒泡到所有依赖这一子集合的父亲等价集合, 更新集合里每个元素的成本并得到新的最优成本和方案。

Volcano和Cascades的区别在于:

  • Cascades合并了逻辑优化和物理优化。Volcano则分为逻辑优化阶段和物理优化阶段,在逻辑优化阶段需穷举逻辑计划,但后期未必用得上,浪费了搜索资源。以此类推,Cascades也不再(严格)区分Logical Operator和Physical Operator,Transform Rule和Implementation Rule

  • Volcano是将枚举所有计划的代价,取代价最小的。Cascades提出了Memo数据结构。Memo此后被广泛采用,成为优化器的核心之一。

在这里插入图片描述

在这里插入图片描述

而每个Group中会存在多个成员,成员通常称之为Group Expression。成员之间是逻辑等价的,也就意味着他们输出的结果是一样的。随着搜索过程的推进,对Operator Tree进行变换时会产生新的Operator Tree,这些Tree仍然存储在Memo中。例如上图的的Group1,既包含了初始的Scan A,也包含了后续搜索产生的TableScan、SortedIDXScan。由于Group引用的是其他Group,这里可以视作形成了一个Group Tree,例如上面的Group 7 引用了 Group3、Group4,Group3又是一个Join算子,引用了Group1、Group2。

在搜索完成之后,我们可以从每个Group中选择出最优的Operator,并递归构建其子节点,即可得到最优的Operator Tree。

总结

本文介绍了具体Calcite 实现对SQL的解析、校验、优化过程,以及基于关系代数对 SQL 查询进行优化的基本原理并结合 Calcite 项目详细介绍了 Volcano/Cascades Optimizer 的设计思路。

参考资料

  1. Apache Calcite:Hadoop 中新型大数据查询引擎
  2. Access Path Selection in a Relational Database Management System
  3. The Volcano Optimizer Generator : Extensibility and Efficient Search
  4. Stream SQL 的执行原理与 Flink 的实现
  5. SQL 查询优化原理与 Volcano Optimizer 介绍
  6. Apache Calcite 处理流程详解(一)
  7. Apache Calcite 优化器详解(二)
  8. 级联火山口:数据库查询优化器初探
  9. Cascades Optimizer
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是学习 Apache Calcite 的一些步骤和资源: 1. 了解 Apache Calcite - 了解 Apache Calcite 的基本概念和特点。可以查看 Apache Calcite 官方网站和文档,了解 Calcite 的功能和用途。 2. 学习 SQL - Apache Calcite 是一个 SQL 解析器和查询优化器,因此了解 SQL 语言的基本语法和特性是非常重要的。可以阅读 SQL 教程或书籍来学习 SQL。 3. 安装和配置 Apache Calcite - 从 Apache Calcite 的官方网站下载最新版本的 Calcite,并按照官方文档的指导进行安装和配置。 4. 编写 SQL 查询 - 编写一些简单的 SQL 查询并在 Apache Calcite 中运行它们,可以使用 Calcite 的命令行界面,也可以在 Java 应用程序中集成 Calcite。 5. 学习 Apache Calcite 的 API - 学习 Apache Calcite 的 API,并尝试使用它们来开发自己的应用程序。可以查看官方文档和示例代码来学习 Calcite 的 API。 6. 参与社区 - 加入 Apache Calcite 的邮件列表和社区,与其他开发者交流,了解 Calcite 的最新动态和发展方向。 推荐一些 Apache Calcite 的学习资源: - Apache Calcite 官方网站:https://calcite.apache.org/ - Apache Calcite 官方文档:https://calcite.apache.org/docs/ - Apache Calcite 示例代码:https://github.com/apache/calcite/tree/master/example - Apache Calcite 的邮件列表:https://calcite.apache.org/mailing-lists.html - SQL 教程:https://www.w3schools.com/sql/ - 《Apache Calcite: A Foundational Framework for Optimized Query Processing》一书,由 Apache Calcite 的核心开发者编写,介绍了 Apache Calcite 的设计和实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值