关系代数是 Calcite 的核心。每个查询都可以表示为一个 关系运算符树。你可以将 SQL 转换为关系代数,也可以直接构建关系运算符树。
优化器规则使用保持 相同语义 的 数学恒等式 来变换表达式树。例如,如果过滤器没有引用其他输入中的列,那么将过滤器推入到内部关联的输入则是有效的。
Calcite 通过反复地将优化器规则应用于关系表达式来优化查询。成本模型指导该过程,优化器引擎生成与原始语义相同,但成本较低的替代表达式。
优化过程是可扩展的。你可以添加自己的 关系运算符、优化器规则、成本模型 和 统计信息。
代数构建器
构建关系表达式的最简单方法是使用代数构建器 RelBuilder。下面是一个例子:
表扫描
1 | final FrameworkConfig config; |
你可以在 RelBuilderExample.java 中找到这个例子和其他例子的完整代码。这段代码打印如下:
1 | LogicalTableScan(table=[[scott, EMP]]) |
它创建了对 EMP 表的扫描,相当于如下 SQL:
1 | SELECT * FROM scott.EMP; |
添加投影
现在,让我们添加一个投影,相当于如下 SQL:
1 | SELECT ename, deptno FROM scott.EMP; |
我们只需要在调用 build 方法前,添加一个 project 方法调用:
1 | final RelNode node = builder.scan("EMP").project(builder.field("DEPTNO"), builder.field("ENAME")).build(); |
输出结果如下:
1 | LogicalProject(DEPTNO=[$7], ENAME=[$1]) |
对 builder.field 的两次调用创建了简单表达式,这些表达式从输入的关系表达式中返回字段。那也就是说,scan 方法的调用创建了 TableScan。Calcite 将它们转换为按序号的字段引用,例如:$7 和 $1。
添加过滤和聚合
下面是一个包含聚合和过滤的查询语句:
1 | final RelNode node = builder.scan("EMP").aggregate(builder.groupKey("DEPTNO"), builder.count(false, "C"), builder.sum(false, "S", builder.field("SAL"))).filter(builder.call(SqlStdOperatorTable.GREATER_THAN, builder.field("C"), builder.literal(10))).build(); |
相当于如下 SQL:
1 | SELECT deptno, count(*) AS c, sum(sal) AS s FROM emp GROUP BY deptno HAVING count(*) > 10 |
并生成如下结果:
1 | LogicalFilter(condition=[>($1, 10)]) |
压栈和出栈
构建器使用 堆栈 来存储第一步生成的关系表达式,并将它作为输入传递给下一步。这允许生成关系表达式的方法生成一个构建器。
在大多数情况下,你只需要使用 build() 这个堆栈方法,用来获取最后一个关系表达式,也就是树的根节点。
有时候堆栈会嵌套得非常深,以至于令人困惑。为了让这些事情清楚明了,你可以从堆栈中去除些表达式。例如,我们正在构建下面这个复杂的连接查询:
1 | join |
我们分三个阶段进行构建。先将中间结果存储在 left 和 right 变量中,然后使用 push() 方法,在创建最终的 Join 对象时,将它们放回堆栈中:
1 | final RelNode left = builder.scan("CUSTOMERS").scan("ORDERS").join(JoinRelType.INNER, "ORDER_ID").build(); |
转换约定
默认的 RelBuilder 会创建没有约定的逻辑 RelNode。但你可以通过 adoptConvention() 来进行切换,从而使用不同的约定:
1 | final RelNode result = builder.push(input).adoptConvention(EnumerableConvention.INSTANCE).sort(toCollation).build(); |
在这个案例中,我们在 input RelNode 之上创建了一个 EnumerableSort。
字段名称和序号
你可以通过名称或序号来引用一个字段。
序号是从零开始的。每个运算符保证它输出字段出现的顺序。例如,Project 返回每个标量表达式生成的字段。
运算符的字段名称需要保证是唯一的,但有时这也意味着,名称并不完全符合你的预期。例如,当你对 EMP 和 DEPT 进行关联时,其中一个输出字段会叫做 DEPTNO,而另一个输出字段则会叫做类似 DEPTNO_1 的名称。
一些关系表达式方法让你能够更好地控制字段名称:
project允许你使用alias(expr, fieldName)来包装表达式。它删除了包装器,但保留了建议的名称(只要它是唯一的);values(String[] fieldNames, Object... values)接受一个字段名称数组。如果数组中的任何元素为空,构建器将会生成一个唯一的名称;
如果一个表达式投影成输入字段,或投影成输入字段的一个转换,那么它将使用输入字段的名称。
一旦唯一的字段名称完成了分配,这些名称就是不可变的。如果你有一个特定的 RelNode 实例,你可以依赖字段名称的不变性。事实上,整个关系表达式也是不可变的。
但是,如果一个关系表达式已经通过了多个重写规则(参考 RelOptRule),结果表达式的字段名称可能看起来与原始表达式不太一样。这种情况下,最好按照序号来引用字段。
当你正在构建一个接受多个输入的关系表达式时,你需要考虑到那些点,从而构建字段引用。这在构建关联条件时经常出现。
假设你正在 EMP 和 DEPT 上构建一个关联查询,EMP 有 8 个字段 EMPNO、ENAME、JOB、MGR、HIREDATE、SAL、COMM、DEPTNO,DEPT 有 3 个字段 DEPTNO、DNAME、LOC。在内部,Calcite 使用偏移量来表示这些字段,存储在一个包含 11 个字段的组合输入行中:左侧输入的第一个字段是 #0(请记住,序号从 0 开始),右侧输入的第一个字段是 #8。
通过构建器 API,你可以指定哪个输入的哪个字段。要引用内部字段序号是 #5 的 SAL,可以写成 builder.field(2, 0, "SAL"),builder.field(2, "EMP", "SAL") 或 builder.field(2, 0, 5)。这个写法表示,在两个输入中,#0 输入的 #5 字段。为什么它需要知道有两个输入?因为它们存储在堆栈中,#1 输入位于堆栈顶部,#0 输入在其下方。如果我们不告诉构建器是两个输入,它不知道 #0 输入的深度。
类似地,要引用内部字段是 #9 (8 + 1) 的 DNAME,可以写成 builder.field(2, 1, "DNAME"),builder.field(2, "DEPT", "DNAME") 或 builder.field(2, 1, 1)。
递归查询
警告:当前 API 是实验性的,如有变更不会另行通知。
下面是一个递归查询的 SQL,用于生成 1, 2, 3, ...10 这样的序列:
1 | WITH RECURSIVE aux(i) AS (VALUES (1) UNION ALL SELECT i + 1 FROM aux WHERE i < 10) SELECT * FROM aux |
可以对 TransientTable 和 RepeatUnion 进行表扫描,来生成这个 SQL:
1 | final RelNode node = builder.values(new String[] {"i"}, 1).transientScan("aux").filter(builder.call(SqlStdOperatorTable.LESS_THAN, builder.field(0), builder.literal(10))).project(builder.call(SqlStdOperatorTable.PLUS, builder.field(0), builder.literal(1))).repeatUnion("aux", true).build(); |
生成结果如下:
1 | LogicalRepeatUnion(all=[true]) |
接口摘要
关系运算符
以下方法会创建一个关系表达式 RelNode,并将它压入堆栈中,然后返回 RelBuilder。
| 方法 | 描述 |
|---|---|
scan(tableName) | 创建一个 TableScan。 |
functionScan(operator, n, expr...)functionScan(operator, n, exprList) | 创建 n 个最新的关系表达式 TableFunctionScan。 |
transientScan(tableName [, rowType]) | 在给定类型的 TransientTable 上创建 TableScan(如果未指定,将使用最新的关系表达式类型)。 |
values(fieldNames, value...)values(rowType, tupleList) | 创建一个 Values。 |
filter([variablesSet, ] exprList)filter([variablesSet, ] expr...) | 在给定谓词的 AND 上创建 过滤器(如果 variablesSet 指定,谓词可以引用这些变量)。 |
project(expr...)project(exprList [, fieldNames]) | 创建一个投影。如果要覆盖默认名称,请使用 alias 来包装表达式,或指定 fieldNames 参数。 |
projectPlus(expr...)projectPlus(exprList) | project 的变体,保留了原始字段,并添加给定的表达式。 |
projectExcept(expr...)projectExcept(exprList) | project 的变体,保留了原始字段,并删除给定的表达式。 |
permute(mapping) | 创建一个使用 mapping 重新排列字段的投影。 |
convert(rowType [, rename]) | 创建一个将字段转换为指定类型,或者重命名这些字段的投影。 |
aggregate(groupKey, aggCall...)aggregate(groupKey, aggCallList) | 创建一个聚合。 |
distinct() | 创建一个消除重复记录的聚合。 |
pivot(groupKey, aggCalls, axes, values) | 添加旋转(pivot 行转列)操作,该操作使用每个度量和值的组合列,生成一个聚合来实现。 |
unpivot(includeNulls, measureNames, axisNames, axisMap) | 添加逆旋转(unpivot 列转行)操作,该操作为每个 Values 生成一个 Join,从而将每行转换为多行来实现。 |
sort(fieldOrdinal...)sort(expr...)sort(exprList) | 创建一个 Sort。在第一种形式中,字段序号是从 0 开始的,负数序号表示降序。例如,-2 表示字段 1 降序。在其它的形式中,你可以将表达式包装在 as,nullsFirst 或 nullsLast 中。 |
sortLimit(offset, fetch, expr...)sortLimit(offset, fetch, exprList) | 创建一个带有 offset 和 limit 的 Sort。 |
limit(offset, fetch) | 创建一个不排序的 Sort,只适用于 offset 和 limit。 |
exchange(distribution) | 创建一个 Exchange。 |
sortExchange(distribution, collation) | 创建一个 SortExchange。 |
correlate(joinType, correlationId, requiredField...)correlate(joinType, correlationId, requiredFieldList) | 使用两个最新的关系表达式,创建一个 Correlate,它包含了一个可变名称以及左侧关联关系需要的字段表达式。 |
join(joinType, expr...)join(joinType, exprList)join(joinType, fieldName...) | 使用两个最新的关系表达式,创建一个 Join。第一种形式,在布尔表达式上进行关联(使用 AND 组合多个条件)。最后一个形式,在命名字段上进行关联,每边必须有一个各自名称的字段。 |
semiJoin(expr) | 使用两个最新的关系表达式,创建一个半连接类型的 Join。 |
antiJoin(expr) | 使用两个最新的关系表达式,创建一个反连接类型的 Join。 |
union(all [, n]) | 使用 n(默认两个)个最新的关系表达式,创建一个 Union。 |
intersect(all [, n]) | 使用 n(默认两个)个最新的关系表达式,创建一个 Intersect。 |
minus(all) | 使用两个最新的关系表达式,创建一个 Minus。 |
repeatUnion(tableName, all [, n]) | 创建与 TransientTable (使用两个最新的关系表达式创建)相关联的 RepeatUnion,它具有 n 个最大迭代次数(默认为 -1,即没有限制)。 |
snapshot(period) | 创建指定的快照时间段的 Snapshot。 |
match(pattern, strictStart, strictEnd, patterns, measures, after, subsets, allRows, partitionKeys, orderKeys, interval) | 创建一个 Match。 |
参数类型:
expr,interval:RexNode;expr...,requiredField...:RexNode数组;exprList,measureList,partitionKeys,orderKeys,requiredFieldList:可迭代的RexNode;fieldOrdinal:行内字段的序号(从 0 开始);fieldName:字段名称,在行内唯一;fieldName...:字符串数组;fieldNames:可迭代的字符串;rowType:RelDataType;groupKey:RelBuilder.GroupKey;aggCall...:RelBuilder.AggCall数组;aggCallList:可迭代的RelBuilder.AggCall;value...:对象数组;value:对象;tupleList:可迭代的RexLiteral集合;all,distinct,strictStart,strictEnd,allRows:布尔值;alias:字符串;correlationId:CorrelationId;variablesSet:可迭代的CorrelationId;varHolder:RexCorrelVariableHolder;patterns:键为字符串,值为RexNode的 Map;subsets:键为字符串,值为字符串有序集合的 Map;distribution:RelDistribution;collation:RelCollation;operator:SqlOperator;joinType:JoinRelType;
builder 方法执行了各种优化,具体包括:
如果要求按顺序投影所有列,
project则返回它的输入;filter会打平条件表达式,所以,一个AND和OR可能有 2 个以上的子节点。filter也会进行简化,例如将x = 1 AND TRUE转化为x = 1;如果你先使用
sort,然后使用limit时,效果就像你调用了sortLimit一样;
有一些注解方法,可以向堆栈顶部的关系表达式添加信息:
| 方法 | 描述 |
|---|---|
as(alias) | 为堆栈顶部的关系表达式分配一个表别名。 |
variable(varHolder) | 创建一个引用顶部关系表达式的相关变量。 |
堆栈方法
| 方法 | 描述 |
|---|---|
build() | 从堆栈中弹出最新创建的关系表达式。 |
push(rel) | 将关系表达式压入堆栈。前面提到的关系方法,例如 scan,会调用这个方法,但是用户代码一般不会调用。 |
pushAll(collection) | 将一组关系表达式压入堆栈。 |
peek() | 返回最新放入堆栈的关系表达式,但不删除它。 |
标量表达式方法
以下方法返回标量表达式 RexNode。许多方法使用堆栈的内容。例如,field("DEPTNO") 返回被添加到堆栈中的关系表达式的 DEPTNO 字段的引用。
| 方法 | 描述 |
|---|---|
literal(value) | 常量。 |
field(fieldName) | 按照名称引用关系表达式最顶层的字段。 |
field(fieldOrdinal) | 按照顺序引用关系表达式最顶层的字段。 |
field(inputCount, inputOrdinal, fieldName) | 按照名称引用关系表达式第 inputCount - inputOrdinal 个字段。 |
field(inputCount, inputOrdinal, fieldOrdinal) | 按照序号引用关系表达式第 inputCount - inputOrdinal 个字段。 |
field(inputCount, alias, fieldName) | 按照表别名和字段名称,引用堆栈顶部最多 inputCount - 1 个元素的字段。 |
field(alias, fieldName) | 按照表别名和字段名称,引用关系表达式最顶层的字段。 |
field(expr, fieldName) | 按照名称引用记录值(record-valued)表达式字段。 |
field(expr, fieldOrdinal) | 按照序号引用记录值(record-valued)表达式字段。 |
fields(fieldOrdinalList) | 按照序号引用输入字段的表达式列表。 |
fields(mapping) | 按照给定映射引用输入字段的表达式列表。 |
fields(collation) | 表达式列表 exprList,sort(exprList) 将复制排序规则。 |
call(op, expr...)call(op, exprList) | 调用函数或运算符。 |
and(expr...)and(exprList) | 逻辑与。会打平嵌套的 AND,并优化涉及 TRUE 和 FALSE 的情况。 |
or(expr...)or(exprList) | 逻辑或。会打平嵌套的 OR,并优化涉及 TRUE 和 FALSE 的情况。 |
not(expr) | 逻辑非。 |
equals(expr, expr) | 等于。 |
isNull(expr) | 检查表达式是否为空。 |
isNotNull(expr) | 检查表达式是否为非空。 |
alias(expr, fieldName) | 重命名表达式(仅作为 project 的参数时有效)。 |
cast(expr, typeName)cast(expr, typeName, precision)cast(expr, typeName, precision, scale) | 将表达式转换为指定类型。 |
desc(expr) | 将排序方向改为降序(仅作为 sort 或 sortLimit 的参数时有效)。 |
nullsFirst(expr) | 将排序顺序改为空值最先(仅作为 sort 或 sortLimit 的参数时有效)。 |
nullsLast(expr) | 将排序顺序改为空值最后(仅作为 sort 或 sortLimit 的参数时有效)。 |
cursor(n, input) | 引用第 input 个(从 0 开始)关系输入,关系输入是有 n 个输入的 TableFunctionScan(参考 functionScan)。 |
模式方法
以下方法会返回用于 match 中的模式。
| 方法 | 描述 |
|---|---|
patternConcat(pattern...) | 连接模式 |
patternAlter(pattern...) | 替换模式 |
patternQuantify(pattern, min, max) | 量化模式 |
patternPermute(pattern...) | 重新排列模式 |
patternExclude(pattern) | 排除模式 |
分组键方法
以下方法会返回一个 RelBuilder.GroupKey。
| 方法 | 描述 |
|---|---|
groupKey(fieldName...)groupKey(fieldOrdinal...)groupKey(expr...)groupKey(exprList) | 创建一个指定表达式的分组键。 |
groupKey(exprList, exprListList) | 创建一个使用分组集合的指定表达式的分组键。 |
groupKey(bitSet [, bitSets]) | 创建一个指定输入列的分组键,如果指定了 bitSets,则指定输入列包含多个分组集合。 |
聚合调用方法
以下方法会返回一个 RelBuilder.AggCall。
| 方法 | 描述 |
|---|---|
aggregateCall(op, expr...)aggregateCall(op, exprList) | 为指定的聚合函数创建一个调用。 |
count([ distinct, alias, ] expr...)count([ distinct, alias, ] exprList) | 为 COUNT 聚合函数创建一个调用。 |
countStar(alias) | 为 COUNT(*) 聚合函数创建一个调用。 |
sum([ distinct, alias, ] expr) | 为 SUM 聚合函数创建一个调用。 |
min([ alias, ] expr) | 为 MIN 聚合函数创建一个调用。 |
max([ alias, ] expr) | 为 MAX 聚合函数创建一个调用。 |
如果想要进一步地修改 AggCall,可以调用如下方法:
| 方法 | 描述 |
|---|---|
approximate(approximate) | 允许聚合的近似值 approximate。 |
as(alias) | 为表达式分配一个列别名(请参考 SQL AS)。 |
distinct() | 在聚合之前消除重复值(请参考 SQL DISTINCT)。 |
distinct(distinct) | 如果配置了 distinct,则在聚合之前消除重复值。 |
filter(expr) | 在聚合之前过滤行(请参考 SQL FILTER (WHERE ...))。 |
sort(expr...) sort(exprList) | 在聚合之前对行进行排序(请参考 SQL WITHIN GROUP)。 |
unique(expr...) unique(exprList) | 在聚合之前使行唯一(请参考 SQL WITHIN DISTINCT)。 |
over() | 将这个 AggCall 转换为窗口聚合(参考下面的 OverCall)。 |
窗口聚合调用方法
为了创建一个 RelBuilder.OverCall(它代表对窗口聚合函数的调用), 需要先创建一个聚合调用,然后调用它的 over() 方法,例如:count().over()。
如果想要进一步地修改 OverCall,可以调用如下方法:
| 方法 | 描述 |
|---|---|
rangeUnbounded() | 创建一个无界的、基于范围的窗口,RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。 |
rangeFrom(lower) | 创建一个基于范围的、有下界的窗口,RANGE BETWEEN lower AND CURRENT ROW。 |
rangeTo(upper) | 创建一个基于范围的、有上界的窗口,RANGE BETWEEN CURRENT ROW AND upper。 |
rangeBetween(lower, upper) | 创建一个基于范围的窗口,RANGE BETWEEN lower AND upper。 |
rowsUnbounded() | 创建一个无界的、基于行的窗口,ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。 |
rowsFrom(lower) | 创建一个基于行的、有下界的窗口,ROWS BETWEEN lower AND CURRENT ROW。 |
rowsTo(upper) | 创建一个基于行的、有上界的窗口,ROWS BETWEEN CURRENT ROW AND upper。 |
rowsBetween(lower, upper) | 创建一个基于行的窗口,ROWS BETWEEN lower AND upper。 |
partitionBy(expr...)partitionBy(exprList) | 根据指定的表达式对窗口进行分区(请参考 SQL PARTITION BY)。 |
orderBy(expr...)sort(exprList) | 对窗口中的行进行排序(请参考 SQL ORDER BY)。 |
allowPartial(b) | 设置是否允许部分宽度的窗口,默认为 true。 |
nullWhenCountZero(b) | 设置如果窗口中没有数据行时,聚合函数是否应该计算为空,默认 false。 |
as(alias) | 分配列别名(请参考 SQL AS),并将 OverCall 转换为 RexNode。 |
toRex() | 将 OverCall 转换为 RexNode。 |
写在最后
笔者因为工作原因接触到 Calcite,前期学习过程中,深感 Calcite 学习资料之匮乏,因此创建了 Calcite 从入门到精通知识星球,希望能够将学习过程中的资料和经验沉淀下来,为更多想要学习 Calcite 的朋友提供一些帮助。

