阅读时间: 2 分钟
Apache Calcite 是一个动态数据管理框架。它提供了一个SQL解析器、验证器和优化器。使用子项目Avatica,我们也有能力对外部数据库执行我们的优化查询。
在SQL查询中的每个行表达式*(rex*)在内部都被定义为一个 "RexNode",它可以是一个标识符、一个字面意思或一个函数。在这篇博客中,我们将说明在SQL查询中只在 "值 "操作符中评估涉及字面意思的函数,这样我们就可以有一个改进的逻辑计划。
使用案例
对于简单的查询,如 "SELECT 5*6,5+6 "或任何只涉及字面的标量函数,其逻辑计划是。
LogicalProject(EXPR$0=[*(5, 6)], EXPR$1=[+(5, 6)])
LogicalValues(tuples=[[{ 0 }]])
优化的计划
-
我们想尽量减少执行引擎所执行的指令总数。
-
我们希望删除多余的操作符。
我们的最终计划应该是这样的。
LogicalValues(tuples=[[{ 30,11 }]])
代码执行
我们将使用火山计划器来实现这一优化。让我们把它连接起来。
自定义特质集
sealed trait MyRel extends RelNode
object MyRel {
val CONVENTION = new Convention.Impl("MyRelTrait", classOf[MyRel])
}
转换器规则
val PROJECT: ConverterRule.Config = ConverterRule.Config.INSTANCE
.withConversion(classOf[LogicalProject], Convention.NONE, MyRel.CONVENTION, "LogicalProjectToMyProject")
.withRuleFactory(_ => LogicalProjectConverter)
val VALUES: ConverterRule.Config = ConverterRule.Config.INSTANCE
.withConversion(classOf[LogicalValues], Convention.NONE, MyRel.CONVENTION, "LogicalValuesToMyLogicalValues")
.withRuleFactory(_ => LogicalValuesConverter)
相关规则
val EVAL_VALUES: EvalValuesRule.Config = RelRule.Config.EMPTY
.withDescription("Evaluate Literals")
.withOperandSupplier(b0 =>
b0.operand(classOf[MyLogicalProject])
.oneInput(b1 =>
b1.operand(classOf[MyLogicalValues])
.noInputs()
)
).as(classOf[EvalValuesRule.Config])
object EvalValuesRule {
trait Config extends RelRule.Config {
override def toRule: RelOptRule = new EvalValuesRule(this)
}
}
杰尼诺RexCompiler
Calcite提供了一个内置的RexCompiler,可以评估一个表达式。它的使用非常简单明了
val compiler = new JaninoRexCompiler(rexBuilder)
val scalar = compiler.compile(expression, inputRowType)
scalar.execute(context)
详细的EVAL_VALUES规则
onMatch
override def onMatch(call: RelOptRuleCall): Unit = {
val project = call.rel(0).asInstanceOf[Project]
val values = call.rel(1).asInstanceOf[Values]
val rexBuilder = project.getCluster.getRexBuilder
val newLiterals = evaluate(project.getProjects, project.getRowType, values, rexBuilder)
call.transformTo(
new MyLogicalValues(
values.getCluster,
project.getRowType,
newLiterals,
project.getTraitSet
)
)
}
评价
def evaluate(
exprs: java.util.List[RexNode],
inputRowType: RelDataType,
values: Values,
rexBuilder: RexBuilder
): ImmutableList[ImmutableList[RexLiteral]] = {
val compiler = new JaninoRexCompiler(rexBuilder)
val execution = exprs.asScala
.map({
case ref: RexInputRef =>
values.getTuples.get(0).get(ref.getIndex).getValue2
case expr =>
val scalar = compiler.compile(ImmutableList.of(expr), inputRowType)
scalar.execute(null)
})
val literalValues = execution.zipWithIndex.map {
case (ref, index) =>
rexBuilder.makeLiteral(ref, inputRowType.getFieldList.get(index).getType, true)
.asInstanceOf[RexLiteral]
}
.asJava
ImmutableList.of(ImmutableList.copyOf(literalValues))
}
这里我们的逻辑很简单,
- 如果表达式是一个RexInputRef,把它映射到一个RexLiteral
- 如果表达式是一个RexLiteral或一个RexCall,使用编译器。
- 使用RexBuilder重新构建RexLiterals。
- 移除 "项目 "操作符,在 "值 "操作符中添加所有的RexLiterals。
把它放在一起
让我们定义一个方法来优化我们的Rel节点
def optimize(relNode: RelNode): RelNode = {
val costPlanner = relNode.getCluster.getPlanner
RelOptUtil.registerDefaultRules(costPlanner, false, false)
costPlanner.addRule(Rules.VALUES.toRule)
costPlanner.addRule(Rules.PROJECT.toRule)
costPlanner.addRule(Rules.EVAL_VALUES.toRule)
val myRel = costPlanner.changeTraits(relNode,relNode.getTraitSet.replace(MyRel.CONVENTION))
costPlanner.setRoot(myRel)
costPlanner.findBestExp()
}
结果
BEFORE :
LogicalProject(EXPR$0=[*(5, 6)], EXPR$1=[+(5, 6)])
LogicalValues(tuples=[[{ 0 }]])
AFTER :
MyLogicalValues(tuples=[[{ 30, 11 }]])
结论
完整的代码可以找到 这里.
我们可以通过使用数以百计的现有规则或为不同的用例添加我们自己的规则,利用基于成本的优化来使用Volcano Planner。一个良好的优化计划是指。
- 尽可能地减少需要处理的行数。
- 移除多余的操作(重复做同样的事情)。
- 有最少的操作要执行(标量函数,聚合函数等)。