Apache Calcite的优化器规则解析
calcite实现的优化器,无论是基于规则的HepPlanner还是基于代价的VolcanoPlanner,它们核心的部分都是要使用规则对关系表达式进行转换。
大部分的可扩展的查询优化系统都会使用规则。规则是一个通用的概念,以一种简单和模块化的方式指定了一种模式,依据关系代数法则对关系表达式进行等价代换。规则使得优化器更加的模块化和易于扩展。
在分析calcite的优化器的优化过程中,详细的了解规则在优化器中是如何匹配关系表达式,如何对关系表达式进行变化等问题是理解优化器优化过程的关键点。
下面分析的代码是基于Apache Calcite的源码1.23.0版本,分析规则在volcanoPlanner优化过程中如何工作的。HepPlanner的规则优化过程流程基本一样,而且更简单。
RelOptRule
在calcite中所有规则类都是派生与基类RelOptRule
。RelOptRule
定义了calcite规则的基本结构和方法。RelOptRule
中包含一个RelOptRuleOperand
的列表,这个RelOptRuleOperand
的列表在规则匹配要变换的关系表达式中有重要作用。RelOptRuleOperand
的列表中的Operand都是有层次结构的,对应着要匹配的关系表达式结构。当规则匹配到了目标的关系表达式后onMatch
方法会被调用,规则生成的新的关系表达式通过RelOptRuleCall
的transform方法让优化器知道关系表达式的变化结果。
RelOptRule的构造函数
public RelOptRule(RelOptRuleOperand operand);
public RelOptRule(RelOptRuleOperand operand, String description)
public RelOptRule(RelOptRuleOperand operand,RelBuilderFactory relBuilderFactory, String description)
对于规则基类提供了上面三个构造函数。其中RelOptRule是最重要的,它要用与判断这个规则是否应该作用于RelNode关系表达式,不可以为null。
RelBuilderFactory是构建关系表达式用的。
RelOptRuleOperand构造函数
protected <R extends RelNode> RelOptRuleOperand(
Class<R> clazz,
RelTrait trait,
Predicate<? super R> predicate,
RelOptRuleOperandChildren children)
<R extends RelNode> RelOptRuleOperand(
Class<R> clazz,
RelTrait trait,
Predicate<? super R> predicate,
RelOptRuleOperandChildPolicy childPolicy,
ImmutableList<RelOptRuleOperand> children)
- clazz: 不可以为空,operand开始匹配时首先会将这个clazz与关系表达式节点RelNode的class进行匹配,为true才能继续后面的匹配逻辑。
- trait: 可以为空,不为空时,RelNode的traitSet要与这个traitSet匹配才能继续后面的匹配规则。
- predicate:自定义的匹配函数,最后只有这个返回true,RelOptRuleOperand的matches才会返回true。
因为关系表达式节点可能有子节点。当你希望某个表达式有符合要求的子节点才匹配时,就需要用到RelOptRuleOperandChildren
里面的operand会去检查子节点是否匹配。
用Rule方法创建Operand
public static <R extends RelNode> RelOptRuleOperand operand(
Class<R> clazz,
RelOptRuleOperand first,
RelOptRuleOperand... rest)
public static <R extends RelNode> RelOptRuleOperand operand(
Class<R> clazz,
RelOptRuleOperandChildren operandList)
public static <R extends RelNode> RelOptRuleOperand operand(
Class<R> clazz,
RelTrait trait,
RelOptRuleOperandChildren operandList)
public static <R extends RelNode> RelOptRuleOperand operandJ(
Class<R> clazz,
RelTrait trait,
Predicate<? super R> predicate,
RelOptRuleOperand first,
RelOptRuleOperand... rest)
public static <R extends RelNode> RelOptRuleOperand operandJ(
Class<R> clazz,
RelTrait trait,
Predicate<? super R> predicate,
RelOptRuleOperandChildren operandList)
- elOptRuleOperand对象都不能直接调用构造函数,应该使用上面这些operand方法。
如果需要匹配的RelNode是没有子节点的可以调用operand(clazz, none())
。如果要匹配的RelNode可以有任意数量的子节点可以调用operand(clazz, any())
。
比如上图的例子,calcite自带的一个规则SortProjectTransposeRule
要匹配sort + project结构的表达式,那么这个规则所带的RelOptRuleOperand结构也要有一样的层次结构才能匹配。
如何生成这样的Operand的结构可以参考以下SortProjectTransposeRule
的构造函数代码。
/** Creates a SortProjectTransposeRule. */
public SortProjectTransposeRule(
Class<? extends Sort> sortClass,
Class<? extends Project> projectClass,
RelBuilderFactory relBuilderFactory, String description) {
this(
operand(sortClass,
operandJ(projectClass, null,
p -> !RexOver.containsOver(p.getProjects(), null),
any())),
relBuilderFactory, description);
}
自定义规则
虽然calcite已经实现了很多规则,但是可能我们在用的的时候需要自定义一些规则,那么实现自定义规则的关键步骤如下:
- 了解要匹配的关系表达式结构,在构造规则的时候生成对应结构的RelOptRuleOperand,如上面的
SortProjectTransposeRule
例子。 - 实现规则的
onMatch
方法,这个方法是在匹配后对关系表达式进行变化时候调用的。在onMatch方法中如果要进行变换那么就要生成新的关系表达式,调用RelOptRuleCall的transform方法通知planner表达式的变换。@Override public void onMatch(RelOptRuleCall call)
- 1
- 在使用planner优化之前将自定义规则添加到planner中,调用planner的
addRule
方法添加。
VolcanoPlanner如何匹配与执行规则
在使用volcanoPlanner或者HepPlanner等优化器之前都需要通过方法addRule
向优化器添加规则,这些优化器才能利用规则。是否在优化器对关系表达式进行优化时,优化器所有的规则都会对这些关系表达式进行匹配一遍呢?答案是否定的,对于VolcanoPlanner它内部会有一个ruleQueue队列,只有在这个队列中的规则才会在优化过程对关系表达式进行匹配检查,如果通过匹配检查后才会执行这个规则的onMatch
方法。
规则如何进入VolcanoPlanner的ruleQueue
在VolcanoPlanner内部,每个关系表达式RelNode要能被VolcanoPlanner优化之前需要进行注册,注册的过程就是生成RelSubset和RelSet记录关系表达式的最优RelNode路径和等价的关系表达式,详细的VolcanoPlanner优化过程解析后面会专门写一片文章讲解。
每个RelNode注册到VolcanoPlanner时候都会调用VolcanoPlanner内部方法registerImpl
。需要注意的是在这个方法开始部分就会使用深度优先算法先将自己的子RelNode注册到VolcanoPlanner中,所有子RelNode注册完之后才会进行后面的代码注册自己到VolcanoPlanner。rel = rel.onRegister
这句代码就是对子节点RelNode进行注册,子节点注册时也会执行到registerImpl
这个方法,也会继续先注册自己的子节点,直到没有子节点为止。
完成子节点的注册或者无子节点时,当前RelNode就开始自己的注册过程。在这个过程也会检查是否有符合匹配RelNode的规则存在,有符合要求的规则存在后就会放入ruleQueue。
检查匹配RelNode class的规则
在registerImpl
方法中当前RelNode会执行registerClass(rel);
,在这个方法中会从volcanoPlanner中的规则查找是否有要匹配当前RelNode class的规则。从这里就可以知道为什么RelOptRule
的RelOptRuleOperand
的构造函数需要一个Class类型的参数了。
public void registerClass(RelNode node) {
final Class<? extends RelNode> clazz = node.getClass();
//classes是planner中的一个集合,有新的class加入这个集合的时候才会调用onNewClass
if (classes.add(clazz)) {
onNewClass(node);
}
if (conventions.add(node.getConvention())) {
node.getConvention().register(this);
}
}
//volcannoPlanner的onNewClass的实现
@Override protected void onNewClass(RelNode node) {
super.onNewClass(node);
final boolean isPhysical = node instanceof PhysicalNode;
// Create mappings so that instances of this class will match existing
// operands.
final Class<? extends RelNode> clazz = node.getClass();
//遍历Planner中的每个规则
for (RelOptRule rule : mapDescToRule.values()) {
if (isPhysical && rule instanceof TransformationRule) {
continue;
}
//使用规则中的每个Operand,只要这个规则中的一个Operand的class
//符合要求就把这个operand加入classOperand集合中
//这里相当于把这个规则加入候选规则,后面还要进一步匹配检查才能决定
//是否将这个规则加入ruleQueue
for (RelOptRuleOperand operand : rule.getOperands()) {
if (operand.getMatchedClass().isAssignableFrom(clazz)) {
classOperands.put(clazz, operand);
}
}
}
}
决定进入ruleQueue的规则
在registerImpl
方法后续会调用fireRules(rel);
,在这个方法中会最终决定之前registerClass
方法中放入classOperands
集合中的operand对应的规则哪些是可以放入ruleQueue中的。
void fireRules(RelNode rel) {
for (RelOptRuleOperand operand : classOperands.get(rel.getClass())) {
if (operand.matches(rel)) {
final VolcanoRuleCall ruleCall;
ruleCall = new DeferringRuleCall(this, operand);
ruleCall.match(rel);
}
}
}
operand的的matches方法中会检查RelNode的class,traitSets(如果operand构造时不为null)是否与operand的匹配,然后再调用operand的predicate lambada方法结果返回位true,那么matches就会返回true。
operand.matches返回为true后,上面的代码显示会创建DeferringRuleCall,并且调用match方法。因为当前operand与当前的RelNode已经匹配,那么operand对应的RelOptRule是否可以匹配当前RelNode还需要检查该规则的其他operand结构是否与RelNode的结构相匹配,DeferringRuleCall的match方法就是做这个工作的。
比如,规则是SortProjectTransposeRule
,当前的operand是RelOperand::sortClass,RelNode是LogicalSort匹配后,在DeferringRuleCall的match方法中会检查它们的子节点RelOperand::projectClass和LogicalProject是否匹配,这里也匹配,那么规则SortProjectTransposeRule
就会被放入ruleQueue。如果是从RelOperand::projectClass和LogicalProject开始,那么就会向上检查RelOperand::sortClass,LogicalSort是否匹配。结构中如果存在分支以深度优先进行检查。
最后如果结构也完全匹配,那么就会调用DeferringRuleCall的onMatch方法将规则放入ruleQueue。
protected void onMatch() {
final VolcanoRuleMatch match =
new VolcanoRuleMatch(
volcanoPlanner,
getOperand0(),
rels,
nodeInputs);
volcanoPlanner.ruleQueue.addMatch(match);
}
- 则的执行
放入ruleQueue的规则在VolcanoPlanner的findBestExp方法中被调用onMatch。需要注意的是,由于RelNode的注册是以深度优先注册,所以匹配子节点的规则会在ruleQueue的前面,执行的时候是以先进先出的方式取出规则,那么就实现了从下到上对关系表达式进行变换,使用动态规划的方式从底层到上层计算最优的代价结果,具体的分析可参考VolcanoPlanner优化器优化过程解析。
总结
规则是Apache Calcite的核心模块中的核心。了解规则在calcite中是如何工作的有助于实现自定义的规则,calcite的易扩展特性一部分就是依靠规则机制实现的。