关于 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 受总多开源框架欢迎的原因之一。
Calcite 概念
在介绍 Calcite 架构之前,先来看下与 Calcite 相关的基础性内容。
关系代数的基本知识
关系代数是关系型数据库操作的理论基础,关系代数支持并、差、笛卡尔积、投影和选择等基本运算。关系代数也是 Calcite 的核心,任何一个查询都可以表示成由关系运算符组成的树。在 Calcite 中,它会先将 SQL 转换成关系表达式(relational expression),然后通过规则匹配(rules match)进行相应的优化,优化会有一个成本(cost)模型为参考。
这里先看下关系代数相关内容,这对于理解 Calcite 很有帮助,特别是 Calcite Optimizer 这块的内容,关系代数的基础可以参考这篇文章 SQL 形式化语言——关系代数,简单总结如下:
查询优化
查询优化主要是围绕着 等价交换 的原则做相应的转换,这部分可以参考【《数据库系统概念(中文第六版)》第13章——查询优化】,关于查询优化理论知识,这里就不再详述,列出一些个人不错不错的博客,大家可以参考一下:
- 数据库查询优化入门: 代数与物理优化基础;
- 高级数据库十五:查询优化器(一);
- 高级数据库十六:查询优化器(二);
- 「 数据库原理 」查询优化(关系代数表达式优化);
- 关系数据库系统的查询优化(1);
- 关系数据库系统的查询优化(10);
Calcite 中的一些概念
Calcite 抛出的概念非常多,笔者最开始在看代码时就被这些概念绕得云里雾里,这时候先从代码的细节里跳出来,先把这些概念理清楚、归归类后再去看代码,思路就清晰很多,因此,在介绍 Calcite 整体实现前,先把这些概念梳理一下,需要对这些概念有个基本的理解,相关的概念如下图所示:
整理如下表所示:
Calcite 架构
关于 Calcite 的架构,可以参考下图(图片来自前面那篇论文),它与传统数据库管理系统有一些相似之处,相比而言,它将数据存储、数据处理算法和元数据存储这些部分忽略掉了,这样设计带来的好处是:对于涉及多种数据源和多种计算引擎的应用而言,Calcite 因为可以兼容多种存储和计算引擎,使得 Calcite 可以提供统一查询服务,Calcite 将会是这些应用的最佳选择。
在 Calcite 架构中,最核心地方就是 Optimizer,也就是优化器,一个 Optimization Engine 包含三个组成部分:
- 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 处理流程
Sql 的执行过程一般可以分为下图中的四个阶段,Calcite 同样也是这样:
但这里为了讲述方便,把 SQL 的执行分为下面五个阶段(跟上面比比又独立出了一个阶段):
- 解析 SQL, 把 SQL 转换成为 AST (抽象语法树),在 Calcite 中用 SqlNode 来表示;
- 语法检查,根据数据库的元数据信息进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树;
- 语义分析,根据 SqlNode 及元信息构建 RelNode 树,也就是最初版本的逻辑计划(Logical Plan);
- 逻辑计划优化,优化器的核心,根据前面生成的逻辑计划按照相应的规则(Rule)进行优化;
- 物理执行,生成物理计划,物理执行计划执行。
这里我们只关注前四步的内容,会配合源码实现以及一个示例来讲解。
示例
示例 SQL 如下:
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();
}
});
Step1: SQL 解析阶段(SQL–>SqlNode)
使用 Calcite 进行 Sql 解析的代码如下:
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 内容可以参考下面这几篇文章,这里就不再详细展开,可以通过下面文章的例子把 JavaCC 的语法了解一下,这样我们也可以自己设计一个 DSL(Doomain Specific Language)。
JavaCC 研究与应用( 8000字 心得 源程序);
JavaCC、解析树和 XQuery 语法,第 1 部分;
JavaCC、解析树和 XQuery 语法,第 2 部分;
编译原理之Javacc使用;
javacc tutorial;
回到 Calcite,Javacc 这里要实现一个 SQL Parser,它的功能有以下两个,这里都是需要在 jj 文件中定义的。
- 设计词法和语义,定义 SQL 中具体的元素;
- 实现词法分析器(Lexer)和语法分析器(Parser),完成对 SQL 的解析,完成相应的转换。
SQL Parser 流程
当 SqlParser 调用 parseStmt() 方法后,其相应的逻辑如下:
// 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 文件中的定义。
//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 对象如下图所示。
Step2: SqlNode 验证(SqlNode–>SqlNode)
经过上面的第一步,会生成一个 SqlNode 对象,它是一个未经验证的抽象语法树,下面就进入了一个语法检查阶段,语法检查前需要知道元数据信息,这个检查会包括表名、字段名、函数名、数据类型的检查。进行语法检查的实现如下:
//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() 方法注册表信息),如下所示:
//org.apache.calcite.schema.impl.AbstractSchema
/**
* Returns a map of tables in this schema by name.
*
* <p>The implementations of {@link #getTableNames()}
* and {@link #getTable(String)} depend on this map.
* The default implementation of this method returns the empty map.
* Override this method to change their behavior.</p>
*
* @return Map of tables in this schema by name
*/
protected Map<String, Table> getTableMap() {
return ImmutableMap.of();
}
//org.apache.calcite.adapter.csvorg.apache.calcite.adapter.csv.CsvSchemasvSchema
//note: 创建表
@Override protected Map<String, Table> getTableMap() {
if (tableMap == null) {
tableMap = createTableMap();
}
return tableMap;
}
CsvSchemasvSchema 中的 getTableMap() 方法通过 createTableMap() 来注册相应的表信息。
结合前面的例子再来分析,在前面定义了 CalciteCatalogReader 实例,该实例就是用来读取 Schema 中的元数据信息的。真正检查的逻辑是在 SqlValidatorImpl 类中实现的,这个 check 的逻辑比较复杂,在看代码时通过两种手段来看:
- DEBUG 的方式,可以看到其方法调用的过程;
- 测试程序中故意构造一些 Case,观察其异常栈。
比如,在示例中 SQL 中,如果把一个字段名写错,写成 ids,其报错信息如下:
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 检查过程
语法检查验证是通过 SqlValidatorImpl 的 validate() 方法进行操作的,其实现如下:
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() 方法中,其实现如下
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 expression,将其标准化,便于后面的逻辑计划优化;
- 注册这个 relational expression 的 scopes 和 namespaces(这两个对象代表了其元信息);
- 进行相应的验证,这里会依赖第二步注册的 scopes 和 namespaces 信息。
Rewrite
关于 Rewrite 这一步,一直困惑比较,因为根据 After unconditional rewrite: 这条日志的结果看,其实前后 SqlNode 并没有太大变化,看 performUnconditionalRewrites() 这部分代码时,看得不是很明白,不过还是注意到了 SqlOrderBy 的注释(注释如下),它的意思是 SqlOrderBy 通过 performUnconditionalRewrites() 方法已经被 SqlSelect 对象中的 ORDER_OPERAND 取代了。
/**
* Parse tree node that represents an {@code ORDER BY} on a query other than a
* {@code SELECT} (e.g. {@code VALUES} or {@code UNION}).
*
* <p>It is a purely syntactic operator, and is eliminated by
* {@link org.apache.calcite.sql.validate.SqlValidatorImpl#performUnconditionalRewrites}
* and replaced with the ORDER_OPERAND of SqlSelect.</p>
*/
public class SqlOrderBy extends SqlCall {
注意到 SqlOrderBy 的原因是因为在 performUnconditionalRewrites() 方法前面都是递归对每个对象进行处理,在后面进行真正的 ransform 时,主要在围绕着 ORDER_BY 这个类型做处理,而且从代码中可以看出,将其类型从 SqlOrderBy 转换成了 SqlSelect,BUDEG 前面的示例,发现 outermostNode 与 topNode 的类型确实发生了变化,如下图所示。
这个方法有个好的地方就是,在不改变原有 SQL Parser 的逻辑的情况下,可以在这个方法里做一些改动,当然如果 SQL Parser 的结果如果直接可用当然是最好的,就不需要再进行一次 Rewrite 了。
registerQuery
这里的功能主要就是将[元数据]转换成 SqlValidator 内部的 对象 进行表示,也就是 SqlValidatorScope 和 SqlValidatorNamespace 两种类型的对象:
- SqlValidatorNamespace:a description of a data source used in a
query,它代表了 SQL 查询的数据源,它是一个逻辑上数据源,可以是一张表,也可以是一个子查询; - SqlValidatorScope:describes the tables and columns accessible at a
particular point in the query,代表了在某一个程序运行点,当前可见的字段名和表名。
这个理解起来并不是那么容易,在 SelectScope 类中有一个示例讲述,这个示例对这两个概念的理解很有帮助。
/**
* <h3>Scopes</h3>
*
* <p>In the query</p>
*
* <blockquote>
* <pre>
* SELECT expr1
* FROM t1,
* t2,
* (SELECT expr2 FROM t3) AS q3
* WHERE c1 IN (SELECT expr3 FROM t4)
* ORDER BY expr4</pre>
* </blockquote>
*
* <p>The scopes available at various points of the query are as follows:</p>
*
* <ul>
* <li>expr1 can see t1, t2, q3</li>
* <li>expr2 can see t3</li>
* <li>expr3 can see t4, t1, t2</li>
* <li>expr4 can see t1, t2, q3, plus (depending upon the dialect) any aliases
* defined in the SELECT clause</li>
* </ul>
*
* <h3>Namespaces</h3>
*
* <p>In the above query, there are 4 namespaces:</p>
*
* <ul>
* <li>t1</li>
* <li>t2</li>
* <li>(SELECT expr2 FROM t3) AS q3</li>
* <li>(SELECT expr3 FROM t4)</li>
*/
validate 验证
接着回到最复杂的一步,就是 outermostNode 实例调用 validate(this, scope) 方法进行验证的部分,对于我们这个示例,这里最后调用的是 SqlSelect 的 validate() 方法,如下所示:
public void validate(SqlValidator validator, SqlValidatorScope scope) {
validator.validateQuery(this, scope, validator.getUnknownType());
}
它调用的是 SqlValidatorImpl 的 validateQuery() 方法
public void validateQuery(SqlNode node, SqlValidatorScope scope,
RelDataType targetRowType) {
final SqlValidatorNamespace ns = getNamespace(node, scope);
if (node.getKind() == SqlKind.TABLESAMPLE) {
List<SqlNode> operands = ((SqlCall) node).getOperandList();
SqlSampleSpec sampleSpec = SqlLiteral.sampleValue(operands.get(1));
if (sampleSpec instanceof SqlSampleSpec.SqlTableSampleSpec) {
validateFeature(RESOURCE.sQLFeature_T613(), node.getParserPosition());
} else if (sampleSpec
instanceof SqlSampleSpec.SqlSubstitutionSampleSpec) {
validateFeature(RESOURCE.sQLFeatureExt_T613_Substitution(),
node.getParserPosition());
}
}
validateNamespace(ns, targetRowType);//note: 检查
switch (node.getKind()) {
case EXTEND:
// Until we have a dedicated namespace for EXTEND
deriveType(scope, node);
}
if (node == top) {
validateModality(node);
}
validateAccess(
node,
ns.getTable(),
SqlAccessEnum.SELECT);
}
/**
* Validates a namespace.
*
* @param namespace Namespace
* @param targetRowType Desired row type, must not be null, may be the data
* type 'unknown'.
*/
protected void validateNamespace(final SqlValidatorNamespace namespace,
RelDataType targetRowType) {
namespace.validate(targetRowType);//note: 验证
if (namespace.getNode() != null) {
setValidatedNodeType(namespace.getNode(), namespace.getType());
}
}
这部分的调用逻辑非常复杂,主要的语法验证是 SqlValidatorScope 部分(它里面有相应的表名、字段名等信息),而 namespace 表示需要进行验证的数据源,最开始的这个 SqlNode 有一个 root namespace,上面的 validateNamespace() 方法会首先调用其 namespace 的 validate() 方法进行验证,以前面的示例为例,这里是 SelectNamespace,其实现如下:
//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() 方法(对本示例而言),其调用过程如下图所示:
Step3: 语义分析(SqlNode–>RelNode/RexNode)
经过第二步之后,这里的 SqlNode 就是经过语法校验的 SqlNode 树,接下来这一步就是将 SqlNode 转换成 RelNode/RexNode,也就是生成相应的逻辑计划(Logical Plan),示例的代码实现如下:
// 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<String> schemaPath,
List<String> viewPath) {
return null;
}
}
为了方便分析,这里也把上面的过程分为以下几步:
- 初始化 RexBuilder;
- 初始化 RelOptPlanner;
- 初始化 RelOptCluster;
- 初始化 SqlToRelConverter;
- 进行转换;
第1、2、4步在上述代码已经有相应的注释,这里不再介绍,下面从第三步开始讲述。
初始化 RelOptCluster
RelOptCluster 初始化的代码如下,这里基本都走默认的参数配置。
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.
*
* <p>For use only from {@link #create} and {@link RelOptQuery}.
*/
RelOptCluster(RelOptPlanner planner, RelDataTypeFactory typeFactory,
RexBuilder rexBuilder, AtomicInteger nextCorrel,
Map<String, RelNode> mapCorrelToRel) {
this.nextCorrel = nextCorrel;
this.mapCorrelToRel = mapCorrelToRel;
this.planner = Objects.requireNonNull(planner);
this.typeFactory = Objects.requireNonNull(typeFactory);
this.rexBuilder = rexBuilder;
this.originalExpression = rexBuilder.makeLiteral("?");
// set up a default rel metadata provider,
// giving the planner first crack at everything
//note: 默认的 metadata provider
setMetadataProvider(DefaultRelMetadataProvider.INSTANCE);
//note: trait(对于 HepPlaner 和 VolcanoPlanner 不一样)
this.emptyTraitSet = planner.emptyTraitSet();
assert emptyTraitSet.size() == planner.getRelTraitDefs().size();
}
SqlToRelConverter 转换
SqlToRelConverter 中的 convertQuery() 将 SqlNode 转换为 RelRoot,其实现如下:
/**
* 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;
* <code>false</code> if the query has already been
* validated.
* @param top Whether the query is top-level, say if its result
* will become a JDBC result set; <code>false</code> if
* the query will be part of a view.
*/
public RelRoot convertQuery(
SqlNode query,
final boolean needsValidation,
final boolean top) {
if (needsValidation) { //note: 是否需要做相应的校验(如果校验过了,这里就不需要了)
query = validator.validate(query);
}
//note: 设置 MetadataProvider
RelMetadataQuery.THREAD_PROVIDERS.set(
JaninoRelMetadataProvider.of(cluster.getMetadataProvider()));
//note: 得到 RelNode(relational expression)
RelNode result = convertQueryRecursive(query, top, null).rel;
if (top) {
if (isStream(query)) {//note: 如果 stream 的话
result = new LogicalDelta(cluster, result.getTraitSet(), result);
}
}
RelCollation collation = RelCollations.EMPTY;
if (!query.isA(SqlKind.DML)) { //note: 如果是 DML 语句
if (isOrdered(query)) { //note: 如果需要做排序的话
collation = requiredCollation(result);
}
}
//note: 对转换前后的 RelDataType 做验证
checkConvertedType(query, result);
if (SQL2REL_LOGGER.isDebugEnabled()) {
SQL2REL_LOGGER.debug(
RelOptUtil.dumpPlan("Plan after converting SqlNode to RelNode",
result, SqlExplainFormat.TEXT,
SqlExplainLevel.EXPPLAN_ATTRIBUTES));
}
final RelDataType validatedRowType = validator.getValidatedNodeType(query);
return RelRoot.of(result, validatedRowType, query.getKind())
.withCollation(collation);
}
真正的实现是在 convertQueryRecursive() 方法中完成的,如下:
/**
* 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 类型,这里会调用下面的方法做相应的转换:
/**
* 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 的各个部分做相应转换,其实现如下:
/**
* 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<SqlNode> orderExprList = new ArrayList<>();
final List<RelFieldCollation> collationList = new ArrayList<>();
//note: 有 order by 操作时
gatherOrderExprs(
bb,
select,
select.getOrderList(),
orderExprList,
collationList);
final RelCollation collation =
cluster.traitSet().canonize(RelCollations.of(collationList));
if (validator.isAggregate(select)) {
//note: 当有聚合操作时,也就是含有 group by、having 或者 Select 和 order by 中含有聚合函数
convertAgg(
bb,
select,
orderExprList);
} else { //note: 对 select list 部分的处理
convertSelectList(
bb,
select,
orderExprList);
}
if (select.isDistinct()) { //note: select 后面含有 DISTINCT 关键字时(去重)
distinctify(bb, true);
}
//note: Converts a query's ORDER BY clause, if any.
convertOrder(
select, bb, collation, orderExprList, select.getOffset(),
select.getFetch());
bb.setRoot(bb.root, true);
}
这里以示例中的 From 部分为例介绍 SqlNode 到 RelNode 的逻辑,按照示例 DEUBG 后的结果如下图所示,因为 form 部分是一个 join 操作,会进入 join 相关的处理中。
这部分方法调用过程是:
convertQuery -->
convertQueryRecursive -->
convertSelect -->
convertSelectImpl -->
convertFrom & convertWhere & convertSelectList
到这里 SqlNode 到 RelNode 过程就完成了,生成的逻辑计划如下:
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]])
到这里前三步就算全部完成了。
Step4: 优化阶段(RelNode–>RelNode)
终于来来到了第四阶段,也就是 Calcite 的核心所在,优化器进行优化的地方,前面 sql 中有一个明显可以优化的地方就是过滤条件的下压(push down),在进行 join 操作前,先进行 filter 操作,这样的话就不需要在 join 时进行全量 join,减少参与 join 的数据量。
关于filter 操作下压,在 Calcite 中已经有相应的 Rule 实现,就是 FilterJoinRule.FilterIntoJoinRule.FILTER_ON_JOIN,这里使用 HepPlanner 作为示例的 planer,并注册 FilterIntoJoinRule 规则进行相应的优化,其代码实现如下:
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 的代码实现做深入的讲述。
HepPlanner
特点(来自 Apache Calcite介绍):
- HepPlanner is a heuristic optimizer similar to Spark’s optimizer,与 spark 的优化器相似,HepPlanner 是一个 heuristic 优化器;
- Applies all matching rules until none can be applied:将会匹配所有的 rules直到一个 rule 被满足;
- Heuristic optimization is faster than cost- based optimization:它比CBO 更快;
- Risk of infinite recursion if rules make opposing changes to the plan:如果没有每次都不匹配规则,可能会有无限递归风险;
VolcanoPlanner
特点(来自 Apache Calcite介绍):
-
VolcanoPlanner is a cost-based optimizer:VolcanoPlanner是一个CBO优化器;
-
Applies matching rules iteratively, selecting the plan with the
cheapest cost on each iteration:迭代地应用 rules,直到找到cost最小的plan; -
Costs are provided by relational expressions;
-
Not all possible plans can be computed:不会计算所有可能的计划;
-
Stops optimization when the cost does not significantly improve through a determinable number of iterations:根据已知的情况,如果下面的迭代不能带来提升时,这些计划将会停止优化;
示例运行结果
经过 HepPlanner 优化后的逻辑计划为:
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,支持不同的存储和计算引擎,目前在业界应用也比较广泛,这也证明其牛叉之处。