Apache Calcite 一条 SQL 的查询计划生成之旅【下】

作者:沈磊(LakeShen),公众号:雷克分析

上一篇文章我们讲解了 Apache Calcite 架构设计及 SQL 优化器概述,这篇文章我们将接着介绍 Apache Calcite 组件的关键原理。

三、Calcite SQL 解析和元数据验证关键原理解析

3.1 Calcite SQL 解析关键原理

当一条 SQL 语句到达引擎,首先通过的是 SQL 解析器,SQL 解析器将用户输入的 SQL 语句转换为一棵抽象语法树,同时在这个过程里,它还会对SQL进行词法和语法校验,如果输入的 SQL 有词法或语法问题,会在这个阶段收到错误提示。

在 Calcite 中,一棵抽象语法树通常用SqlNode来表示。以下面这条 SQL 为例:

img

这条SQL在经过 Calcite 解析后,形成的 AST 抽象语法树如下:

img

图中的每一个节点都是一个 SqlNode,SqlNode作为抽象语法树的顶级抽象,相互关联组成了一颗大的抽象语法树。

Calcite 支持通过两种方式进行 SQL 解析:即基于 Java CC(Java Compiler Compiler)的 SQL 解析以及基于 Anltr 的 SQL 解析。Calcite 默认基于 Java CC 来做 SQL 解析:Calcite 能够基于Parser.jj 文件,使用 Java CC 技术,动态生成 SQL 解析实现类(XXXParserImpl),Calcite 本身内置有一个 Parser.jj 文件,如果我们需要自定义 SQL 语法的时候,我们也可以使用 FMPP 技术,通过自定义 ftl(freemaker 语言模板)和 config.fmpp 文件的内容,来控制最终需要生成的Parser.jj 的内容,从而实现自定义 SQL 语法。

config.fmpp 主要定义 SQL 语法中的关键字、外部引入的类(如自定义的 SqlNode)、以及一些 SQL 解析方法定义等等,具体的实现一般会在 ftl 文件中。以 Apahce Flink 项目举例,组成其 SQL 解析器模块的主要文件如下:

img

3.2 Calcite SQL 校验和关系代数转换流程

3.2.1 Calcite SQL 元数据校验

上文我们提到:SQL 解析后会形成一棵 SqlNode 的抽象语法树,下一步我们需要对这棵树进行元数据验证,以校验SQL的合法性。这里需要注意,经过元数据验证之后的SqlNode,其本质还是一棵SqlNode 树,之后依然需要经过 SqlNode 到RelNode关系代数的转换,才会形成一棵由RelNode组成的关系代数节点树。

在 Calcite 元数据验证阶段,其主要验证三个点:

  1. 对 SQL 语句中的 Table Schema 进行校验,如 Table 存不存在,Column 存不存在
  2. 对 SQL 语句中函数进行校验,如函数是否存在
  3. 针对数据类型的校验,如函数中的参数数据类型和函数定义是否匹配

Calcite 中元数据验证源码的核心方法入口为:SqlValidatorImpl.validate(),SqlValidatorImpl的使用,需要配合着SqlOperatorTable,SqlValidatorCatalogReader,RelDataTypeFactory三个类进行使用。

下面是SqlValidatorImpl 的初始化构造器的源码如下:

img

初始化 SqlValidatorImpl 需要传入三类参数:

  1. SqlOperatorTable-是生产函数和 SQL Operator 的工厂,为SqlValidatorImpl 提供函数、SQL 操作符(比如 >,=,<)的元数据相关信息
  2. SqlValidatorCatalogReader - 提供 Table Schema 和 Rowcount 等相关信息,其中CatalogReader 封装了从底层的元数据存储中读取 Table Schema 的数据接口
  3. RelDataTypeFactory 和RelDataTypeSystem – 为SqlValidatorImpl 提供所有需要识别的数据类型,以及精度相关的信息。

要验证一个 SQL 中用到的函数,我们可以从自定义实现的SqlOperatorTable中进行查找,一般有两种方法:lookupOperatorOverloads 和getOperatorList ,lookupOperatorOverloads主要逻辑是将某类函数的具体信息查找出来,并添加到输入参数operatorList 集合中。

img

对于数据类型以及默认的精度等信息,在Calcite中可以从RelDataTypeFactory和RelDataTypeSystem 中进行获取,一般这两个接口需要自定义扩展实现。虽然 Calcite 中已经有这两个接口的默认实现,但一般我们需要的类型以及精度信息,不会和 Calcite 内置实现完全一样,所以还是需要自己复写这两个接口中的部分方法。

SqlValidatorCatalogReader统一对上层元数据验证阶段提供了 Table 信息获取接口,同时屏蔽了底层获取具体 Table 元数据的实现细节。根据 SQL 查询语句中 From 后面表的 paths,调用该接口,可获取 Table 的 Schema 信息、Table 的 RowCount 信息,以及PreparingTable等信息。

3.2.2 Calcite 关系代数转换流程

进行元数据校验之后,我们得到的还是一棵SqlNode 抽象语法树,接下来我们需要将其转换为RelNode 关系代数 Tree,后续才能基于关系代数优化理论对其进行优化。

关系代数转化阶段和元数据验证阶段是紧密在一起的,经过元数据验证阶段后,每个SqlNode的具体输出的字段和数据类型信息都被添加至SqlValidatorImpl 中的nodeToTypeMap 集合中,这样从SqlNode转到相应的RelNode时,每个RelNode 的数据类型,也都能够被 bind 上。

Calcite 使用SqlToRelConverter将SqlNode 转换为RelNode ,其入口方法是convertQuery(),之后会调用convertQueryRecursive方法,底层最终调用 convertXXX 方法(如下图所示),递归遍历抽象语法树将SqlNode 转换为RelNode ,至此,SqlNode 转 RelNode 转换完毕。

img

四、Calcite SQL 执行计划生成与优化

4.1 Calcite RBO 启发式优化

SqlNode转换为RelNode关系代数节点树后,Calcite会对其分别进行 RBO(Rule-Based Optimization)和 CBO (Cost-Based Optimization)优化。RBO 优化一般是会结合一些预定且有收益的逻辑优化规则,一般发生在 CBO 优化之前,提前对计划进行逻辑优化。RBO 改变的是计划的逻辑,而不是物理实现,这里只是对逻辑计划做等价转换。如果需要更改计划的物理实现(Convention),需要结合ConverterRule来进行实现。

下面以 Calcite FilterIntoJoinRule 优化规则进行举例,FilterIntoJoinRule 规则主要尝试将 Join 之上的 Filter 过滤条件,下推到 Join RelNode 下面,这样的好处是能够提前对数据进行过滤,减少进入 Join RelNode 的数据量,从而降低计算成本。下图左边部分是输入的原始计划,右边部分是是经过FilterIntoJoinRule 优化后的计划,可以看到,Filter RelNode 已经下推到 Join 之下了。

img

在Calcite 中,RBO 优化主要使用 HepPlanner 来实现,在HepPlanner 优化之前,会将 SQL Query 计划 Tree 中的每一个 RelNode,先转换为 HepRelVertex 节点,通过 HepPlanner 的 setRoot 方法,从根节点,递归的将所有 RelNode 加入到图中,同时根据每个 RelNode 以及输入信息,构建图的边,在这个过程中,构建 HepRelVertex,再构建出一个 DAG(有向无环图),然后从设置的根节点的 RelNode,按照预设定遍历顺序,进行 RBO 优化。

img

那么 Calicte 为什么会将RelNode 转换为HepRelVertex 呢?因为在进行 RBO 的时候,需要记录RelNode转换前与转换后的相关信息,而RelNode本身只能记录其输入信息,无法记录其输出的节点信息,所以这里引入 HepRelVertex 。

Calcite HepPlanner 节点优化遍历方式有以下四种:

img

  1. ARBITRARY,DEPTH-FIRST – 本质上两者底层都是从深度优先遍历和优化,核心区别在于:当一个规则作用于计划时,两者都会从新产生的节点进行遍历,顺序为 DEPTH-FIRST 时,如果之前已经应用过某个规则,那么在之后转换的新的计划 Tree 中,不会再使用该规则,所以它比 ARBITRARY 效率更高。
  2. BOTTOM-UP – 从叶子节点到根节点,自低向上进行遍历和适用规则优化。
  3. TOP-DOWN – 从根节点到叶子阶段,自顶向下进行遍历优化。

最终经过 RBO 优化,会产生一棵优化后的逻辑计划的关系代数Tree。当然,RBO 优化也有一定的缺陷性,因为 RBO 优化阶段更倾向于一定有收益的优化规则,往往会导致可运用的优化规则是有限的,其适用场景相对有限。同时 RBO 由于只是根据先验经验来选择适用一定的规则集合,没有结合计划实际适用的计算资源来进行判定,可能在某些场景下,RBO 优化会适得其反。

4.2 Calcite 物化视图改写

在 RBO 优化之后,可以使用物化视图来对查询进行优化。物化视图本质是特定逻辑下的预计算结果。Calcite 中支持两种物化视图改写方式:基于 UnifyRule 的局部匹配替换改写和基于MaterializedViewRule 结构化信息改写。前者可以在 CBO 优化之前单独进行优化,配合可用的物化RelOptMaterialization 集合,使用SubstitutionVisitor 来进行改写。基于MaterializedViewRule结构化信息改写,从本质上来讲,它就是优化规则的一种实现,需要添加到VolcanoPlanner 优化规则集合中,和 CBO 优化一起来进行配合使用。

使用基于UnifyRule 进行局部匹配替换的物化改写时,需要传入三类信息:物化视图的计算逻辑 A、SQL 查询计算逻辑 B、代替物化视图计算逻辑的 C,其原理概括来说,是在 SQL 查询计划中,自顶向下找到和物化视图计算计划中相等的子集顶层的RelNode E,然后从 E 之上,使用UnifyRule,自低向上变换 Query 的计划结构,最终在 SQL 查询计划中,使用计划 C 来替换 B 。

img

UnifyRule 局部匹配替换改写的实现,整体相对清晰和简洁,而且改写的速度也相对较快,Calcite 内置的 UnifyRule 已经可以支持大部分场景。但其缺点也很明显,其改写范围本质上就是取决于UnifyRule的数量和所覆盖的场景,对于有很多 Join 或者其他非常复杂的 SQL 查询,UnifyRule 可能就不支持,此时需要我们自定义UnifyRule 来Case By Case 的支持,因此UnifyRule 通用性相对较低。

基于MaterializedViewRule 结构化信息改写,会首先将 SQL 查询转换为 SPJG(Join-Select-Project-GroupBy)的标准形式,然后提取查询中的 Join,Projects,Filters,Grouping 和 Aggregations 五种表达式,运用一系列的步骤分别与物化视图对应的表达式进行匹配和改写。由于是将查询转换为标准形式,那么在其之上扩展的逻辑也会相对更加通用,使用范围会更广。但缺点在于其技术实现原理过于底层,需要和 CBO 优化器配合使用,会导致对物化视图改写逻辑跟踪和问题排查难度较大,逻辑相对比较黑盒,上手成本会比较高。同时可能存在对于部分 SQL 逻辑复杂的场景(比如存在 10 + 以上表关联),改写效率会比UnifyRule 低。

4.3 Calcite CBO 基于代价的优化

经过 RBO 优化或者物化视图改写之后的执行计划, 本质上还是一个逻辑执行计划,所以这个阶段需要经过 CBO 优化器,选出一个 Cost 最低的物理计划。既然涉及到 Cost,肯定涉及到 Calcite 的物理转换。在 Calcite 中,该过程通过Convention来进行实现,一般在 CBO 阶段后,我们需要自定义相应的Convention来定义转换逻辑,以下是 Apache Flink 的Convention举例:

img

实现一个 CBO 优化器,包含以下几块关键内容:

  1. 搜索框架(比如 Volcano 搜索框架、Cascades 搜索框架),如何快速从众多计划空间中,找到最优物理计划。
  2. Cost Model,每种 RelNode 自身的 Cost 如何计算,基数如何评估,以及如何结合统计信息模型,如何让计算的 Cost 更加贴近真实计算。
  3. 优化规则集合,包括 Transformation Rule(逻辑计划的优化规则)和 Implementation Rule(逻辑 RelNode 转物理 RelNode 优化规则)
  4. 物理转换信息,Calcite 里面使用RelTrait 来表示物理特质信息,Calicte 内置有三种物理特质:Convention (一般是物理实现,比如 Flink、Spark)、RelCollation (排序信息)、RelDistribution

一般在流式计算中,主要会用到 RBO 规则进行优化,因为流式系统的处理数据是动态且不确定的,会导致 Cost 评估不准。在 OLAP 分析或者批处理系统中,RBO 和 CBO 会一起结合使用来对 SQL Query 计划优化。

Calcite 中使用 RelOptCost 作为优化器 Cost 的基础接口,默认有 rowCount(数据行数)、CPU(CPU 的消耗)、IO(磁盘 IO 消耗),我们也可以通过实现RelOptCost 接口,自定义我们的 Cost Model,一般 SQL 优化器 Cost 除了 CPU、磁盘 IO 消耗外,还有内存以及网络 IO,最终代价还取决于优化器优化的资源类型。下面以 Flink 中的 Cost 举例:

img

那么一个RelNode Tree 的 Cost 怎么计算呢,在 Calcite 中,RelNode 接口有computeSelfCost 方法,底层具体实现的RelNode,我们可以复写该方法,来计算自身的 Cost 成本。其默认实现为:逻辑计划(LogicalXX 开头),其 Cost 成本为无限大,同时它的 Convention 为Convention.None。一棵RelNode 关系代数 Tree 的成本,就是这棵关系代数 Tree 中所有RelNode节点的自身 Cost 之和,即每个 RelNode computeSelftCost 值相加。

img

Calcite 的 Cost 计算以及 Cost 计算相关元数据统一使用RelMetadataQuery 进行封装,BuiltInMetadata 类中定义了每一类元数据的接口,比如 Selectivity(选择率)、RowCount(行数)、DistinctRowCount(去重后的行数)等等,最后通过实现MetadataHandler来自定义元数据的计算逻辑,这个阶段可以结合统计信息服务来使用。

Calcite 使用RelSet和RelSubSet 来存储等价计划以及某一种物理特质下最优的物理计划。在优化前,会将 RelNode Tree 中每一个 RelNode 注册到 VolcanoPlanner 中,都会有一个RelSubset(Convention 为 Convention.None),随着后续结合 ConverterRule(物理转换规则),会更新RelSubset 中 Best 所指的计划,最后结合动态规划,找到最优的物理计划。

五、Calcite SQL 方言转换

Calcite 中 SQL 方言转换最重要的两个类是RelToSqlConverter 和 SqlDialect ,其中RelToSqlConverter 会将RelNode转换为SqlNode,SqlDialect 则是统一了对 SqlNode方言翻译的所有方法,我们可以自定义复现SqlDialect 的方法,来控制方言转换的具体行为,在 Calcite 内部,已经有很多数据引擎的SqlDialect方言转换的实现,但本身实现逻辑较少,要想生产级别的使用,还需要自定义扩展内部的实现逻辑,下图是 Calcite 内置的 SqlDialect 类:

img

要实现对自定义 SQL 方言的转换,核心是在相应引擎的SqlDialect方言转化类中进行自定义扩展。默认情况下,每个SqlNode都会有unparse 方法,我们可在此方法中定义其转换为相应 SQL 的实现:

img

下图是 Calcite RelNode 转换为 SQL 方言的示例代码:

img

// 我们自定义的 RelToSqlConverter 和 SqlDialect
XXXRelToSqlConverter relToSqlConverter = new XXXRelToSqlConverter(XXXSqlDialect);
SqlNode sqlNode = relToSqlConverter.visitRoot(rel).asStatement();
return sqlNode.toSqlString(XXXSqlDialect).getSql()

SQL 方言转换的核心源码调用时序图如下:

img

六、总结

Apache Calcite 作为一款成熟的开源项目,已经在众多的 Apache 和商业项目中进行使用(下图是使用了Calcite 的项目):

img

与此同时,大量商业产品也在使用 Calcite,广泛的应用也验证了 Calcite 项目自身的成熟性。使用 Calcite 能够降低开发 SQL Base 的项目复杂度,在 SQL 层,我们无需重复对 SQL 解析、元数据验证、RBO 和 CBO 等底层框架造轮子,大大地提高了开发效率。

更多精彩内容,欢迎关注我的公粽号: 【雷克分析】 ,关注私信发送:命令、提效、数据库、提示词、calcite、论文,有学习资料等着你 ,欢迎关注

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/501969.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LLM+RAG: 关于知识问答优化的思考总结

本文分享实践中对LLMRAG实现知识问答系统的相关调研和思考。 LLM的知识库问答有3种实现路径&#xff1a;RAG 或 微调&#xff0c;或两者结合。而RAG和微调都各有利弊&#xff0c;比如说&#xff1a; - RAG&#xff1a; 低成本易部署&#xff0c;适用于知识会更新的场景&#…

C语言----动态内存管理(2)

1.这里总结动态内存管理里面的错误 &#xff08;1&#xff09;使用malloc开辟空间以后直接赋值 这个就是malloc开辟失败返回空指针&#xff0c;直接给空指针赋值就是错误的&#xff0c; tip1:使用malloc开辟空间以后一定要判断是否为空 &#xff08;2&#xff09; 越界访问…

Qt应用软件【测试篇】cppchecker静态代码检查

文章目录 cppcheker简介下载地址与安装检查项目QT Creator使用CPP Cheker开启检查常见错误总结错误信息说明cppcheker简介 Cppcheck 是一个用于 C/C++ 代码的分析工具。它提供独特的代码分析以检测错误,并专注于检测未定义的行为和危险的编码结构。其目标是仅检测代码中的真实…

itertools, 一个超好用的Python库

前言 Python用来处理迭代器的工具你想到了啥&#xff1f;itertools 就是一个特别有用的库&#xff0c;它提供了一系列用于创建和操作迭代器的工具&#xff0c;以下是10个常用的操作&#xff0c;可用在实际工作中&#xff0c;熟练掌握这些操作&#xff0c;将极大提升你在 Pytho…

带你快速初步了解Python列表

1.列表 列表主要是用来存储多个数据&#xff0c;是有序的集合 2.创建列表 """ 语法&#xff1a;变量名 [数据1,数据2,数据3......] 注意&#xff1a;列表中的数据类型可以是各种不同的数据类型 """ 创建空列表 list1 [] print(list1) …

【Ai生态开发】Spring AI上架,打造专属业务大模型,AI开发再也不是难事!

大家好 这里是苏泽 后端是工作 ai是兴趣 对于ai的产生我的立场是拥抱ai的 是希望拿他作为提升能力的工具 那么这一篇带大家来学习如何使用ai打造一个专属的业务大模型 需求 就是说假设现在有一个 商城系统 里面有查询订单的api和获取商品购买方式的api 用户只需要输入 “…

【Windows】打开ftp服务器上的文件夹时发生错误,请检查是否有权限

【Windows】打开ftp服务器上的文件夹时发生错误&#xff0c;请检查是否有权限 ftp协议有两种工作方式&#xff1a;port方式和pasv方式&#xff0c;中文意思为主动式和被动式。 port&#xff08;主动&#xff09;方式的连接过程是&#xff1a;客户端向服务器的ftp端口&#xf…

Unity 游戏设计模式:工厂模式

本文由 简悦 SimpRead 转码&#xff0c; 原文地址 mp.weixin.qq.com 工厂模式是一种创建型设计模式&#xff0c;它提供了一种封装对象实例化过程的方式&#xff0c;使得客户端代码与具体类的实现解耦。 在 C# 的游戏设计中&#xff0c;模式有以下作用&#xff1a; 对象的创建…

3.1作业

作业要求&#xff1a; 通过w(红色臂角度增大)s&#xff08;红色臂角度减小&#xff09;d&#xff08;蓝色臂角度增大&#xff09;a&#xff08;蓝色臂角度减小&#xff09;按键控制机械臂 程序代码&#xff1a; #include<myhead.h> #define SER_IP "192.168.126.…

一文讲透:可视化大屏中3D元素的融入和使用方法

在可视化大屏中&#xff0c;3D元素融入的越来越多&#xff0c;贝格前端工场经常接到这类项目&#xff0c;很多老铁认为加个3D效果很easy&#xff0c;其实不然&#xff0c;工序非常复杂&#xff0c;总结如下。 一、什么是3D技术 三维展示&#xff08;3D展示&#xff09;是指使用…

Codeforces Round 930 (Div. 2)题解

A. Shuffle Party&#xff08;Problem - A - Codeforces&#xff09; 题目大意&#xff1a;给定一个n长数组&#xff0c;并使得a[i]i&#xff0c;现在定义一种操作swap(k):找出k的最大不等于自己的除数d&#xff0c;交换a[k]和a[d]&#xff0c;k从1开始直到n结束&#xff0c;问…

灯塔:HTML笔记

网页由哪些部分组成&#xff1f; *文字 图片 音频 视频 超链接 程序员写的代码是通过浏览器转换成网页的 五大浏览器有哪些&#xff1f; *IE浏览器 *火狐浏览器&#xff08;Firefox&#xff09; *谷歌浏览器&#xff08;Chrome&#xff09; *Safari浏览器 *欧朋浏览器&…