
Apache Calcite 概览(三)--代数
代数
关系代数是 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 |
程序的输出结果是:
1 | LogicalProject(DEPTNO=[$7], ENAME=[$1]) |
对 builder.field 的两次调用创建了简单表达式,这些表达式从输入的关系表达式中返回字段,即调用 scan 方法创建的 TableScan 输入。
Calcite 按顺序将返回的字段转换为字段引用,分别为 $7 和 $1。
添加过滤器和聚合操作
包含聚合和过滤操作的查询:
1 | final RelNode node = builder |
等价的 SQL 语句为:
1 | SELECT deptno, count(*) AS c, sum(sal) AS s |
输出的结果为:
1 | LogicalFilter(condition=[>($1, 10)]) |
推入和弹出操作
构造器方法使用堆栈存储某一步生成的关系表达式,并将其作为输入传递给下一步。这允许产生关系表达式的方法来负责创建构造器。
大多数时间里,我们只会使用堆栈中的 build() 方法,来获取最后的关系表达式,即关系表达式树的根节点。
有些场景下,堆栈会变得比较深,容易使我们产生困惑。为了使事情简单明了,我们也可以从堆栈中删除表达式。例如,在这里我们正在建立一个复杂的联接:
1 | . |
我们分三个阶段进行构建。将中间结果存储在左右变量中,并在需要创建最终 Join 的时候使用 push() 方法将它们放回堆栈中:
1 | final RelNode left = builder |
转换规则
默认的 RelBuilder 会创建没有约定的逻辑 RelNode。但是我们可以通过 adoptConvention() 方法来切换使用不同的约定规则:
1 | final RelNode result = builder |
在这种场景下,我们在输入 RelNode 的顶部创建一个 EnumerableSort。
属性的名称和序号
我们可以通过名称或者是序号来索引某个属性。
序号是从 0 开始。每个运算符都保证其输出字段的出现顺序。例如,Project 返回由每个标量表达式生成的字段。
运算符中的属性名称需要确保是唯一的,但是有时这意味着名称并不完全符合我们的预期。例如,当我们通过 DEPTNO 字段对 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 内部,Calcite 将这些字段表示为具有 11 个字段的组合输入行中的偏移量:左侧输入的第一个属性是 #0(以 0 为基准,请牢记),右侧输入的第一个属性是 #8。
但是通过构建器的 API,我们可以指定输入的字段。为了索引到 SAL,内部的属性是 #5,可以写做 builder.field(2, 0, "SAL")、builder.field(2, "EMP", "SAL") 或者是 builder.field(2, 0, 5)。含义是“两个输入,#0 输入中 #5 属性”。(为什么需要知道有两个输入?因为它们存储在堆栈中;输入 #1 在堆栈的顶部,输入 #0 在堆栈的底部。如果我们不告诉构建器这是两个输入,那么它将不知道输入 #0 栈有多深。)
类似的,为了索引 DNAME 属性,内部属性是 #9(8 + 1),可以写做 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 ( |
可以使用对 TransientTable 和 RepeatUnion 的扫描操作来生成:
1 | final RelNode node = builder |
输出结果:
1 | LogicalRepeatUnion(all=[true]) |
API 概述
关系运算符
如下的一些方法能够创建关系表达式(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 上创建 Filter;如果指定了变量集合,则谓词可以引用到这些变量 |
| project(expr…) project(exprList [, fieldNames]) | 创建一个 Project 对象。如果要覆盖默认名称,需要使用别名包装表达式,或指定 fieldNames 参数。 |
| projectPlus(expr…) projectPlus(exprList) | 保留原始字段并附加给定表达式的项目变体。 |
| projectExcept(expr…) projectExcept(exprList) | 保留原始字段并删除给定表达式的项目变体。 |
| permute(mapping) | 创建一个 Project 对象,并使用 mapping 参数排列属性。 |
| convert(rowType [, rename]) | 创建一个将字段转换为给定类型的 Project,还可以重命名它们。 |
| aggregate(groupKey, aggCall…) aggregate(groupKey, aggCallList) | 创建一个 Aggregate。 |
| distinct() | 创建一个可以消除重复记录的 Aggregate 对象。 |
| pivot(groupKey, aggCalls, axes, values) | 添加枢轴操作,该操作是通过为带有度量和值的每种组合列生成一个聚合来实现。 |
| sort(fieldOrdinal…) sort(expr…) sort(exprList) | 创建一个 Sort 对象。 在第一种形式中,字段序号从0开始,负序号表示下降。例如,-2表示字段1降序。 在其他形式中,可以将表达式包装为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) | 使用 SEMI 连接类型创建两个最新关系表达式的 Join。 |
| antiJoin(expr) | 使用 ANTI 连接类型创建两个最新关系表达式的 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,intervalRexNodeexpr...,requiredField...RexNode 数组exprList,measureList,partitionKeys,orderKeys,requiredFieldList可迭代的 RexNodefieldOrdinal其行内字段的序数(从0开始)fieldName属性的名称,在其行内是唯一的fieldName...字符串类型数组fieldNames可迭代的字符串rowTypeRelDataTypegroupKeyRelBuilder.GroupKeyaggCall...RelBuilder.AggCall 的数组aggCallList可迭代的 RelBuilder.AggCallvalue...对象数组value对象tupleList可迭代的 List 集合的 RexLiteralall,distinct,strictStart,strictEnd,allRowsbooleanalias字符串correlationIdCorrelationIdvariablesSet可迭代的 CorrelationIdvarHolderRexCorrelVariable 对象中的 Holderpatterns一个 Map 对象,key 是字符串,value 是 RexNode 对象subsets一个 Map 对象,key 是字符串,value 是排序好的字符串集合。distributionRelDistributioncollationRelCollationoperatorSqlOperatorjoinTypeJoinRelType
构建器方法执行各种优化,包括:
project如果要求按顺序投影所有列,则返回其输入filter展平条件(因此 AND 和 OR 可能有两个以上的孩子结点),简化了(将 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) | 按名称引用记录值表达式的字段 |
| field(expr, fieldOrdinal) | 按属性顺序引用记录值表达式的字段 |
| 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) | 将排序顺序更改为 null first(仅作为 sort 或 sortLimit 的参数有效) |
| nullsLast(expr) | 将排序顺序更改为 null last(仅作为 sort 或 sortLimit 的参数有效) |
| cursor(n, input) | 引用具有 n 个输入的 TableFunctionScan 的 input(从 0 开始)关系输入(请参见 functionScan) |
模式方法
如下方法返回用于匹配的模式。
| 方法 | 描述 |
|---|---|
| patternConcat(pattern…) | 级联模式 |
| patternAlter(pattern…) | 备用模式 |
| patternQuantify(pattern, min, max) | 量化模式 |
| patternPermute(pattern…) | 排列模式 |
| patternExclude(pattern) | 排除模式 |
key 分组方法
如下的方法将返回 RelBuilder.GroupKey 对象。
| 方法 | 描述 |
|---|---|
| groupKey(fieldName…) groupKey(fieldOrdinal…) groupKey(expr…) groupKey(exprList) | 创建给定表达式的 group key |
| groupKey(exprList, exprListList) | 创建具有分组集的给定表达式的 group key |
| groupKey(bitSet [, bitSets]) | 创建给定输入列的 group key,如果指定了 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 为 true,在执行聚合操作前消除重复记录 |
| filter(expr) | 在执行聚合操作前过滤行记录(参见 SQL FILTER (WHERE ...)) |
| sort(expr…) sort(exprList) | 在执行聚合操作前排序行记录(参见 SQL WITHIN GROUP) |