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
,interval
RexNodeexpr...
,requiredField...
RexNode 数组exprList
,measureList
,partitionKeys
,orderKeys
,requiredFieldList
可迭代的 RexNodefieldOrdinal
其行内字段的序数(从0开始)fieldName
属性的名称,在其行内是唯一的fieldName...
字符串类型数组fieldNames
可迭代的字符串rowType
RelDataTypegroupKey
RelBuilder.GroupKeyaggCall...
RelBuilder.AggCall 的数组aggCallList
可迭代的 RelBuilder.AggCallvalue...
对象数组value
对象tupleList
可迭代的 List 集合的 RexLiteralall
,distinct
,strictStart
,strictEnd
,allRows
booleanalias
字符串correlationId
CorrelationIdvariablesSet
可迭代的 CorrelationIdvarHolder
RexCorrelVariable 对象中的 Holderpatterns
一个 Map 对象,key 是字符串,value 是 RexNode 对象subsets
一个 Map 对象,key 是字符串,value 是排序好的字符串集合。distribution
RelDistributioncollation
RelCollationoperator
SqlOperatorjoinType
JoinRelType
构建器方法执行各种优化,包括:
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 ) |