转载自:http://matt33.com/2019/03/07/apache-calcite-process-flow/
关于 Apache Calcite 的简单介绍可以参考 Apache Calcite:Hadoop 中新型大数据查询引擎 这篇文章,Calcite 一开始设计的目标就是 one size fits all,它希望能为不同计算存储引擎提供统一的 SQL 查询引擎,当然 Calcite 并不仅仅是一个简单的 SQL 查询引擎,在论文 Apache Calcite: A Foundational Framework for Optimized Query Processing Over Heterogeneous Data Sources 的摘要(摘要见下面)部分,关于 Calcite 的核心点有简单的介绍,Calcite 的架构有三个特点:flexible, embeddable, and extensible,就是灵活性、组件可插拔、可扩展,它的 SQL Parser 层、Optimizer 层等都可以单独使用,这也是 Calcite 受总多开源框架欢迎的原因之一。
Apache Calcite is a foundational software framework that provides query processing, optimization, and query language support to many popular open-source data processing systems such as Apache Hive, Apache Storm, Apache Flink, Druid, and MapD. Calcite’s architecture consists of
- a modular and extensible query optimizer with hundreds of built-in optimization rules,
- a query processor capable of processing a variety of query languages,
- an adapter architecture designed for extensibility,
- and support for heterogeneous data models and stores (relational, semi-structured, streaming, and geospatial).
This flexible, embeddable, and extensible architecture is what makes Calcite an attractive choice for adoption in bigdata frameworks. It is an active project that continues to introduce support for the new types of data sources, query languages, and approaches to query processing and optimization.
在介绍 Calcite 架构之前,先来看下与 Calcite 相关的基础性内容。
关系代数是关系型数据库操作的理论基础,关系代数支持并、差、笛卡尔积、投影和选择等基本运算。关系代数也是 Calcite 的核心,任何一个查询都可以表示成由关系运算符组成的树。在 Calcite 中,它会先将 SQL 转换成关系表达式(relational expression),然后通过规则匹配(rules match)进行相应的优化,优化会有一个成本(cost)模型为参考。
这里先看下关系代数相关内容,这对于理解 Calcite 很有帮助,特别是 Calcite Optimizer 这块的内容,关系代数的基础可以参考这篇文章 SQL 形式化语言——关系代数,简单总结如下:
名称 | 英文 | 符号 | 说明 |
---|---|---|---|
选择 | select | σ | 类似于 SQL 中的 where |
投影 | project | Π | 类似于 SQL 中的 select |
并 | union | ∪ | 类似于 SQL 中的 union |
集合差 | set-difference | - | SQL中没有对应的操作符 |
笛卡儿积 | Cartesian-product | × | 类似于 SQL 中不带 on 条件的 inner join |
重命名 | rename | ρ | 类似于 SQL 中的 as |
集合交 | intersection | ∩ | SQL中没有对应的操作符 |
自然连接 | natural join | ⋈ | 类似于 SQL 中的 inner join |
赋值 | assignment | ← |
查询优化主要是围绕着 等价交换 的原则做相应的转换,这部分可以参考【《数据库系统概念(中文第六版)》第13章——查询优化】,关于查询优化理论知识,这里就不再详述,列出一些个人不错不错的博客,大家可以参考一下:
Calcite 抛出的概念非常多,笔者最开始在看代码时就被这些概念绕得云里雾里,这时候先从代码的细节里跳出来,先把这些概念理清楚、归归类后再去看代码,思路就清晰很多,因此,在介绍 Calcite 整体实现前,先把这些概念梳理一下,需要对这些概念有个基本的理解,相关的概念如下图所示:
calcite 基本概念
整理如下表所示:
类型 | 描述 | 特点 |
---|---|---|
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 树 | 代表了对数据的一个处理操作,常见的操作有 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 可以提供统一查询服务,Calcite 将会是这些应用的最佳选择。
Calcite Architecture,图片来自论文
在 Calcite 架构中,最核心地方就是 Optimizer,也就是优化器,一个 Optimization Engine 包含三个组成部分:
Sql 的执行过程一般可以分为下图中的四个阶段,Calcite 同样也是这样:
但这里为了讲述方便,把 SQL 的执行分为下面五个阶段(跟上面比比又独立出了一个阶段):
这里我们只关注前四步的内容,会配合源码实现以及一个示例来讲解。
示例 SQL 如下:
1 2 3 4 |
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 |
这里有两张表,其表各个字段及类型定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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(); } }); |
使用 Calcite 进行 Sql 解析的代码如下:
1 2 |
SqlParser parser = SqlParser.create(sql, SqlParser.Config.DEFAULT); SqlNode sqlNode = parser.parseStmt(); |
Calcite 使用 JavaCC 做 SQL 解析,JavaCC 根据 Calcite 中定义的 Parser.jj 文件,生成一系列的 java 代码,生成的 Java 代码会把 SQL 转换成 AST 的数据结构(这里是 SqlNode 类型)。
与 Javacc 相似的工具还有 ANTLR,JavaCC 中的 jj 文件也跟 ANTLR 中的 G4文件类似,Apache Spark 中使用这个工具做类似的事情。
关于 Javacc 内容可以参考下面这几篇文章,这里就不再详细展开,可以通过下面文章的例子把 JavaCC 的语法了解一下,这样我们也可以自己设计一个 DSL(Doomain Specific Language)。
回到 Calcite,Javacc 这里要实现一个 SQL Parser,它的功能有以下两个,这里都是需要在 jj 文件中定义的。
当 SqlParser 调用 parseStmt()
方法后,其相应的逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// org.apache.calcite.sql.parser.SqlParser public SqlNode parseStmt() throws SqlParseException { return parseQuery(); } public SqlNode parseQuery() throws SqlParseException { try { return parser.parseSqlStmtEof(); //note: 解析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 文件中的定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//org.apache.calcite.sql.parser.impl.SqlParserImpl public SqlNode parseSqlStmtEof() throws Exception { return SqlStmtEof(); } /** * Parses an SQL statement followed by the end-of-file symbol. * note:解析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"); } //note: 解析 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"); } |
示例中 SQL 经过前面的解析之后,会生成一个 SqlNode,这个 SqlNode 是一个 SqlOrder 类型,DEBUG 后的 SqlOrder 对象如下图所示。
SqlNode 结果
经过上面的第一步,会生成一个 SqlNode 对象,它是一个未经验证的抽象语法树,下面就进入了一个语法检查阶段,语法检查前需要知道元数据信息,这个检查会包括表名、字段名、函数名、数据类型的检查。进行语法检查的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//note: 二、sql validate(会先通过Catalog读取获取相应的metadata和namespace) //note: get metadata and namespace SqlTypeFactoryImpl factory = new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT); CalciteCatalogReader calciteCatalogReader = new CalciteCatalogReader( CalciteSchema.from(rootScheme), CalciteSchema.from(rootScheme).path(null), factory, new CalciteConnectionConfigImpl(new Properties())); //note: 校验(包括对表名,字段名,函数名,字段类型的校验。) SqlValidator validator = SqlValidatorUtil.newValidator(SqlStdOperatorTable.instance(), calciteCatalogReader, factory, conformance(frameworkConfig)); SqlNode validateSqlNode = validator.validate(sqlNode); |
我们知道 Calcite 本身是不管理和存储元数据的,在检查之前,需要先把元信息注册到 Calcite 中,一般的操作方法是实现 SchemaFactory,由它去创建相应的 Schema,在 Schema 中可以注册相应的元数据信息(如:通过 getTableMap()
方法注册表信息),如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//org.apache.calcite.schema.impl.AbstractSchema /** * Returns a map of tables in this schema by name. * * |
CsvSchemasvSchema 中的 getTableMap()
方法通过 createTableMap()
来注册相应的表信息。
结合前面的例子再来分析,在前面定义了 CalciteCatalogReader 实例,该实例就是用来读取 Schema 中的元数据信息的。真正检查的逻辑是在 SqlValidatorImpl
类中实现的,这个 check 的逻辑比较复杂,在看代码时通过两种手段来看:
比如,在示例中 SQL 中,如果把一个字段名写错,写成 ids,其报错信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
org.apache.calcite.runtime.CalciteContextException: From line 1, column 156 to line 1, column 158: Column 'IDS' not found in table 'J' at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at org.apache.calcite.runtime.Resources$ExInstWithCause.ex(Resources.java:463) at org.apache.calcite.sql.SqlUtil.newContextException(SqlUtil.java:787) at org.apache.calcite.sql.SqlUtil.newContextException(SqlUtil.java:772) at org.apache.calcite.sql.validate.SqlValidatorImpl.newValidationError(SqlValidatorImpl.java:4788) at org.apache.calcite.sql.validate.DelegatingScope.fullyQualify(DelegatingScope.java:439) at org.apache.calcite.sql.validate.SqlValidatorImpl$Expander.visit(SqlValidatorImpl.java:5683) at org.apache.calcite.sql.validate.SqlValidatorImpl$Expander.visit(SqlValidatorImpl.java:5665) at org.apache.calcite.sql.SqlIdentifier.accept(SqlIdentifier.java:334) at org.apache.calcite.sql.util.SqlShuttle$CallCopyingArgHandler.visitChild(SqlShuttle.java:134) at org.apache.calcite.sql.util.SqlShuttle$CallCopyingArgHandler.visitChild(SqlShuttle.java:101) at org.apache.calcite.sql.SqlOperator.acceptCall(SqlOperator.java:865) at org.apache.calcite.sql.validate.SqlValidatorImpl$Expander.visitScoped(SqlValidatorImpl.java:5701) at org.apache.calcite.sql.validate.SqlScopedShuttle.visit(SqlScopedShuttle.java:50) at org.apache.calcite.sql.validate.SqlScopedShuttle.visit(SqlScopedShuttle.java:33) at org.apache.calcite.sql.SqlCall.accept(SqlCall.java:138) at org.apache.calcite.sql.util.SqlShuttle$CallCopyingArgHandler.visitChild(SqlShuttle.java:134) at org.apache.calcite.sql.util.SqlShuttle$CallCopyingArgHandler.visitChild(SqlShuttle.java:101) at org.apache.calcite.sql.SqlOperator.acceptCall(SqlOperator.java:865) at org.apache.calcite.sql.validate.SqlValidatorImpl$Expander.visitScoped(SqlValidatorImpl.java:5701) at org.apache.calcite.sql.validate.SqlScopedShuttle.visit(SqlScopedShuttle.java:50) at org.apache.calcite.sql.validate.SqlScopedShuttle.visit(SqlScopedShuttle.java:33) at org.apache.calcite.sql.SqlCall.accept(SqlCall.java:138) at org.apache.calcite.sql.validate.SqlValidatorImpl.expand(SqlValidatorImpl.java:5272) at org.apache.calcite.sql.validate.SqlValidatorImpl.validateWhereClause(SqlValidatorImpl.java:3977) at org.apache.calcite.sql.validate.SqlValidatorImpl.validateSelect(SqlValidatorImpl.java:3305) at org.apache.calcite.sql.validate.SelectNamespace.validateImpl(SelectNamespace.java:60) at org.apache.calcite.sql.validate.AbstractNamespace.validate(AbstractNamespace.java:84) at org.apache.calcite.sql.validate.SqlValidatorImpl.validateNamespace(SqlValidatorImpl.java:977) at org.apache.calcite.sql.validate.SqlValidatorImpl.validateQuery(SqlValidatorImpl.java:953) at org.apache.calcite.sql.SqlSelect.validate(SqlSelect.java:216) at org.apache.calcite.sql.validate.SqlValidatorImpl.validateScopedExpression(SqlValidatorImpl.java:928) at org.apache.calcite.sql.validate.SqlValidatorImpl.validate(SqlValidatorImpl.java:632) at com.matt.test.calcite.test.SqlTest3.sqlToRelNode(SqlTest3.java:200) at com.matt.test.calcite.test.SqlTest3.main(SqlTest3.java:117) Caused by: org.apache.calcite.sql.validate.SqlValidatorException: Column 'IDS' not found in table 'J' at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at org.apache.calcite.runtime.Resources$ExInstWithCause.ex(Resources.java:463) at org.apache.calcite.runtime.Resources$ExInst.ex(Resources.java:572) ... 33 more java.lang.NullPointerException at org.apache.calcite.plan.hep.HepPlanner.addRelToGraph(HepPlanner.java:806) at org.apache.calcite.plan.hep.HepPlanner.setRoot(HepPlanner.java:152) at com.matt.test.calcite.test.SqlTest3.main(SqlTest3.java:124) |
语法检查验证是通过 SqlValidatorImpl 的 validate()
方法进行操作的,其实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
org.apache.calcite.sql.validate.SqlValidatorImpl //note: 做相应的语法树校验 public SqlNode validate(SqlNode topNode) { //note: root 对应的 Scope SqlValidatorScope scope = new EmptyScope(this); scope = new CatalogScope(scope, ImmutableList.of("CATALOG")); //note: 1.rewrite expression //note: 2.做相应的语法检查 final SqlNode topNode2 = validateScopedExpression(topNode, scope); //note: 验证 final RelDataType type = getValidatedNodeType(topNode2); Util.discard(type); return topNode2; } |
主要的实现是在 validateScopedExpression()
方法中,其实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private SqlNode validateScopedExpression( SqlNode topNode, SqlValidatorScope scope) { //note: 1. rewrite expression,将其标准化,便于后面的逻辑计划优化 SqlNode outermostNode = performUnconditionalRewrites(topNode, false); cursorSet.add(outermostNode); top = outermostNode; TRACER.trace("After unconditional rewrite: {}", outermostNode); //note: 2. Registers a query in a parent scope. //note: register scopes and namespaces implied a relational expression if (outermostNode.isA(SqlKind.TOP_LEVEL)) { registerQuery(scope, null, outermostNode, outermostNode, null, false); } //note: 3. catalog 验证,调用 SqlNode 的 validate 方法, outermostNode.validate(this, scope); if (!outermostNode.isA(SqlKind.TOP_LEVEL)) { // force type derivation so that we can provide it to the // caller later without needing the scope deriveType(scope, outermostNode); } TRACER.trace("After validation: {}", outermostNode); return outermostNode; } |
它的处理逻辑主要分为三步:
Rewrite
关于 Rewrite 这一步,一直困惑比较,因为根据 After unconditional rewrite:
这条日志的结果看,其实前后 SqlNode 并没有太大变化,看 performUnconditionalRewrites()
这部分代码时,看得不是很明白,不过还是注意到了 SqlOrderBy 的注释(注释如下),它的意思是 SqlOrderBy 通过 performUnconditionalRewrites()
方法已经被 SqlSelect 对象中的 ORDER_OPERAND
取代了。
1 2 3 4 5 6 7 8 9 |
/** * Parse tree node that represents an {@code ORDER BY} on a query other than a * {@code SELECT} (e.g. {@code VALUES} or {@code UNION}). * * |
注意到 SqlOrderBy 的原因是因为在 performUnconditionalRewrites()
方法前面都是递归对每个对象进行处理,在后面进行真正的 ransform 时,主要在围绕着 ORDER_BY 这个类型做处理,而且从代码中可以看出,将其类型从 SqlOrderBy 转换成了 SqlSelect,BUDEG 前面的示例,发现 outermostNode 与 topNode 的类型确实发生了变化,如下图所示。
这个方法有个好的地方就是,在不改变原有 SQL Parser 的逻辑的情况下,可以在这个方法里做一些改动,当然如果 SQL Parser 的结果如果直接可用当然是最好的,就不需要再进行一次 Rewrite 了。
registerQuery
这里的功能主要就是将[元数据]转换成 SqlValidator 内部的 对象 进行表示,也就是 SqlValidatorScope 和 SqlValidatorNamespace 两种类型的对象:
这个理解起来并不是那么容易,在 SelectScope 类中有一个示例讲述,这个示例对这两个概念的理解很有帮助。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/** *
Namespaces* *In the above query, there are 4 namespaces: * *
|
validate 验证
接着回到最复杂的一步,就是 outermostNode 实例调用 validate(this, scope)
方法进行验证的部分,对于我们这个示例,这里最后调用的是 SqlSelect 的 validate()
方法,如下所示:
1 2 3 |
public void validate(SqlValidator validator, SqlValidatorScope scope) { validator.validateQuery(this, scope, validator.getUnknownType()); } |
它调用的是 SqlValidatorImpl 的 validateQuery()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public void validateQuery(SqlNode node, SqlValidatorScope scope, RelDataType targetRowType) { final SqlValidatorNamespace ns = getNamespace(node, scope); if (node.getKind() == SqlKind.TABLESAMPLE) { List |
这部分的调用逻辑非常复杂,主要的语法验证是 SqlValidatorScope 部分(它里面有相应的表名、字段名等信息),而 namespace 表示需要进行验证的数据源,最开始的这个 SqlNode 有一个 root namespace,上面的 validateNamespace()
方法会首先调用其 namespace 的 validate()
方法进行验证,以前面的示例为例,这里是 SelectNamespace,其实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//org.apache.calcite.sql.validate.AbstractNamespace public final void validate(RelDataType targetRowType) { switch (status) { case UNVALIDATED: //note: 还没开始 check try { status = SqlValidatorImpl.Status.IN_PROGRESS; //note: 更新当前 namespace 的状态 Preconditions.checkArgument(rowType == null, "Namespace.rowType must be null before validate has been called"); RelDataType type = validateImpl(targetRowType); //note: 检查验证 Preconditions.checkArgument(type != null, "validateImpl() returned null"); setType(type); } finally { status = SqlValidatorImpl.Status.VALID; } break; case IN_PROGRESS: //note: 已经开始 check 了,死循环了 throw new AssertionError("Cycle detected during type-checking"); case VALID://note: 检查结束 break; default: throw Util.unexpected(status); } } //org.apache.calcite.sql.validate.SelectNamespace //note: 检查,还是调用 SqlValidatorImpl 的方法 public RelDataType validateImpl(RelDataType targetRowType) { validator.validateSelect(select, targetRowType); return rowType; } |
最后验证方法的实现是 SqlValidatorImpl 的 validateSelect()
方法(对本示例而言),其调用过程如下图所示:
验证部分的处理流程
经过第二步之后,这里的 SqlNode 就是经过语法校验的 SqlNode 树,接下来这一步就是将 SqlNode 转换成 RelNode/RexNode,也就是生成相应的逻辑计划(Logical Plan),示例的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// create the rexBuilder final RexBuilder rexBuilder = new RexBuilder(factory); // init the planner // 这里也可以注册 VolcanoPlanner,这一步 planner 并没有使用 HepProgramBuilder builder = new HepProgramBuilder(); RelOptPlanner planner = new HepPlanner(builder.build()); //note: init cluster: An environment for related relational expressions during the optimization of a query. final RelOptCluster cluster = RelOptCluster.create(planner, rexBuilder); //note: init SqlToRelConverter final SqlToRelConverter.Config config = SqlToRelConverter.configBuilder() .withConfig(frameworkConfig.getSqlToRelConverterConfig()) .withTrimUnusedFields(false) .withConvertTableAccess(false) .build(); //note: config // 创建 SqlToRelConverter 实例,cluster、calciteCatalogReader、validator 都传进去了,SqlToRelConverter 会缓存这些对象 final SqlToRelConverter sqlToRelConverter = new SqlToRelConverter(new DogView(), validator, calciteCatalogReader, cluster, StandardConvertletTable.INSTANCE, config); // convert to RelNode RelRoot root = sqlToRelConverter.convertQuery(validateSqlNode, false, true); root = root.withRel(sqlToRelConverter.flattenTypes(root.rel, true)); final RelBuilder relBuilder = config.getRelBuilderFactory().create(cluster, null); root = root.withRel(RelDecorrelator.decorrelateQuery(root.rel, relBuilder)); RelNode relNode = root.rel; //DogView 的实现 private static class DogView implements RelOptTable.ViewExpander { public DogView() { } @Override public RelRoot expandView(RelDataType rowType, String queryString, List |
为了方便分析,这里也把上面的过程分为以下几步:
第1、2、4步在上述代码已经有相应的注释,这里不再介绍,下面从第三步开始讲述。
RelOptCluster 初始化的代码如下,这里基本都走默认的参数配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
org.apache.calcite.plan.RelOptCluster /** Creates a cluster. */ public static RelOptCluster create(RelOptPlanner planner, RexBuilder rexBuilder) { return new RelOptCluster(planner, rexBuilder.getTypeFactory(), rexBuilder, new AtomicInteger(0), new HashMap<>()); } /** * Creates a cluster. * * |
SqlToRelConverter 中的 convertQuery()
将 SqlNode 转换为 RelRoot,其实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
/** * Converts an unvalidated query's parse tree into a relational expression. * note:把一个 parser tree 转换为 relational expression * @param query Query to convert * @param needsValidation Whether to validate the query before converting; * |
真正的实现是在 convertQueryRecursive()
方法中完成的,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
/** * Recursively converts a query to a relational expression. * note:递归地讲一个 query 转换为 relational expression * * @param query Query * @param top Whether this query is the top-level query of the * statement * @param targetRowType Target row type, or null * @return Relational expression */ protected RelRoot convertQueryRecursive(SqlNode query, boolean top, RelDataType targetRowType) { final SqlKind kind = query.getKind(); switch (kind) { case SELECT: return RelRoot.of(convertSelect((SqlSelect) query, top), kind); case INSERT: return RelRoot.of(convertInsert((SqlInsert) query), kind); case DELETE: return RelRoot.of(convertDelete((SqlDelete) query), kind); case UPDATE: return RelRoot.of(convertUpdate((SqlUpdate) query), kind); case MERGE: return RelRoot.of(convertMerge((SqlMerge) query), kind); case UNION: case INTERSECT: case EXCEPT: return RelRoot.of(convertSetOp((SqlCall) query), kind); case WITH: return convertWith((SqlWith) query, top); case VALUES: return RelRoot.of(convertValues((SqlCall) query, targetRowType), kind); default: throw new AssertionError("not a query: " + query); } } |
依然以前面的示例为例,因为是 SqlSelect 类型,这里会调用下面的方法做相应的转换:
1 2 3 4 5 6 7 8 9 10 |
/** * Converts a SELECT statement's parse tree into a relational expression. * note:将一个 Select parse tree 转换成一个关系表达式 */ public RelNode convertSelect(SqlSelect select, boolean top) { final SqlValidatorScope selectScope = validator.getWhereScope(select); final Blackboard bb = createBlackboard(selectScope, null, top); convertSelectImpl(bb, select);//note: 做相应的转换 return bb.root; } |
在 convertSelectImpl()
方法中会依次对 SqlSelect 的各个部分做相应转换,其实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/** * Implementation of {@link #convertSelect(SqlSelect, boolean)}; * derived class may override. */ protected void convertSelectImpl( final Blackboard bb, SqlSelect select) { //note: convertFrom convertFrom( bb, select.getFrom()); //note: convertWhere convertWhere( bb, select.getWhere()); final List |
这里以示例中的 From 部分为例介绍 SqlNode 到 RelNode 的逻辑,按照示例 DEUBG 后的结果如下图所示,因为 form 部分是一个 join 操作,会进入 join 相关的处理中。
convertFrom 之 Join 的情况
这部分方法调用过程是:
1 2 3 4 5 |
convertQuery --> convertQueryRecursive --> convertSelect --> convertSelectImpl --> convertFrom & convertWhere & convertSelectList |
到这里 SqlNode 到 RelNode 过程就完成了,生成的逻辑计划如下:
1 2 3 4 5 6 |
LogicalSort(sort0=[$0], dir0=[ASC]) LogicalProject(USER_ID=[$0], USER_NAME=[$1], USER_COMPANY=[$5], USER_AGE=[$2]) LogicalFilter(condition=[AND(>($2, 30), >($3, 10))]) LogicalJoin(condition=[=($1, $4)], joinType=[inner]) LogicalTableScan(table=[[USERS]]) LogicalTableScan(table=[[JOBS]]) |
到这里前三步就算全部完成了。
终于来来到了第四阶段,也就是 Calcite 的核心所在,优化器进行优化的地方,前面 sql 中有一个明显可以优化的地方就是过滤条件的下压(push down),在进行 join 操作前,先进行 filter 操作,这样的话就不需要在 join 时进行全量 join,减少参与 join 的数据量。
关于filter 操作下压,在 Calcite 中已经有相应的 Rule 实现,就是 FilterJoinRule.FilterIntoJoinRule.FILTER_ON_JOIN
,这里使用 HepPlanner 作为示例的 planer,并注册 FilterIntoJoinRule 规则进行相应的优化,其代码实现如下:
1 2 3 4 5 |
HepProgramBuilder builder = new HepProgramBuilder(); builder.addRuleInstance(FilterJoinRule.FilterIntoJoinRule.FILTER_ON_JOIN); //note: 添加 rule HepPlanner hepPlanner = new HepPlanner(builder.build()); hepPlanner.setRoot(relNode); relNode = hepPlanner.findBestExp(); |
在 Calcite 中,提供了两种 planner:HepPlanner 和 VolcanoPlanner,关于这块内容可以参考【Drill/Calcite查询优化系列】这几篇文章(讲述得非常详细,赞),这里先简单介绍一下 HepPlanner 和 VolcanoPlanner,后面会关于这两个 planner 的代码实现做深入的讲述。
特点(来自 Apache Calcite介绍):
特点(来自 Apache Calcite介绍):
经过 HepPlanner 优化后的逻辑计划为:
1 2 3 4 5 6 7 |
LogicalSort(sort0=[$0], dir0=[ASC]) LogicalProject(USER_ID=[$0], USER_NAME=[$1], USER_COMPANY=[$5], USER_AGE=[$2]) LogicalJoin(condition=[=($1, $4)], joinType=[inner]) LogicalFilter(condition=[>($2, 30)]) EnumerableTableScan(table=[[USERS]]) LogicalFilter(condition=[>($0, 10)]) EnumerableTableScan(table=[[JOBS]]) |
可以看到优化的结果是符合我们预期的,HepPlanner 和 VolcanoPlanner 详细流程比较复杂,后面会有单独的文章进行讲述。
Calcite 本身的架构比较好理解,但是具体到代码层面就不是那么好理解了,它抛出了很多的概念,如果不把这些概念搞明白,代码基本看得也是云里雾里,特别是之前没有接触过这块内容的同学(我最开始看 Calcite 代码时是真的头大),入门的门槛确实高一些,但是当这些流程梳理清楚之后,其实再回头看,也没有多少东西,在生产中用的时候主要也是针对具体的业务场景扩展相应的 SQL 语法、进行具体的规则优化。
Calcite 架构设计得比较好,其中各个组件都可以单独使用,Rule(规则)扩展性很强,用户可以根据业务场景自定义相应的优化规则,它支持标准的 SQL,支持不同的存储和计算引擎,目前在业界应用也比较广泛,这也证明其牛叉之处。
本文只是个人理解的总结,由于本人也是刚接触这块,理解有偏差的地方,欢迎指正~