目录
背景
优化规则ProjectFilterPullUpConstantsRule
- matches方法逻辑详解
- onMatch方法逻辑详解
- rewriteProjects投用重写详解
总结
背景
这篇文章分享基于成本优化器CBO可插拔式优化规则ProjectFilterPullUpConstantsRule,从此Rule命名来看,从Project投影(Select 从句)和Filter谓词(Where条件)这种SQL语句写法中上拉常量。
举例:
SELECT id,name,age,100 as score
FROM student
WHERE age = 18 and name <> '张三'
这是Sql语句最基本的写法,那么这里何为常量Constants,哪里常量能上拉,上拉到哪里以及如何优化?这是本篇文章的重点。
为了说明方便,这里使用SQL进行讲述,其实优化器内部使用的RelNode关系表达式构造的操作符树组成来构建的。但是常量上拉是基于操作符树父与子的构建关系来确定上下的。
转换为操作符树,形式为:
Project(id,name,age,100 as score ) Project(id,name,18,100 as score)
| |
Filter(age = 18 and name <> '张三' ) Filter(age = 18 and name <> '张三' )
| |
TableScan(student) TableScan(student)
图1
从Project角度来说,就是把Filter(Where条件)中常量上拉到Project投影(即Select字段中)图1右图蓝色为优化变换后。当然这些操作变换都得满足匹配条件和等价变换的前提的。
Hive几乎所有优化规则Rule继承了父类RelOptRule。关于RelOptRule和RelOptRuleCall相关概念。这里不再赘述,详细翻阅前期文章。也需实现两个方法matches和OnMatch两个方法。matches默认实现返回true。
优化规则ProjectFilterPullUpConstantsRule
因为matches和OnMatch两个方法是每条优化规则的关键,这里还是做一些两个方法的说明。
1)matches方法逻辑详解
matches方法返回此规则Rule是否可能与给定的操作数operands匹配。优化器在匹配上规则Rule的所有操作数Operands之后和调用OnMatch(ReloptRuleCall)之前调用此方法。在优化器的实现中,它可能会在调用OnMatch(ReloptRuleCall)之前将匹配的ReloptRuleCall排队很长时间,matches方法提前判断这种方法是有好处的,因为优化器可以在处理的早期,把不满足匹配条件的规则放弃掉。
判断由RelOptCall调用的优化规则Rule是否与输入参数RelNode关系表达式匹配,即此优化规则Rule能否应用到一个RelNode关系表达式树上。ProjectFilterPullUpConstantsRule的判断条件如下:
call.rel(1)取得Filter谓词,并判断此谓词条件是否为确定性的。如果此谓词是非确定性的,则不满足匹配条件,放弃优化。
所谓谓词条件的确定性,是如果对该运算符的调用保证在给定相同操作数operand时始终返回相同的结果,即为确定性。非确定性如select a from table where a = random() 谓词中 a = random() 随机函数,每次返回的结果都是非确定性的。
代码语言:javascript复制public boolean matches(RelOptRuleCall call) {
final Filter filterRel = call.rel(1);
RexNode condition = filterRel.getCondition();
if (!HiveCalciteUtil.isDeterministic(condition)) {//谓词条件是确定性的
return false;
}
return super.matches(call);
}
但是此方法的任何实现都可以给出误报,也就是说虽然规则与操作数匹配,但随后具有OnMatch(ReloptRuleCall)而不生成任何后续任务。下文会讲到matches虽然匹配上,但是Project如果没有上拉成功,则会rerun;提前结束,不做任何优化操作。
2)onMatch方法逻辑详解
正如上图1所示,顶层是Project投影,子RelNode是Filter谓词。rewriteProjects方法是进行常量上拉最为关键的部分,其对Project进行了重写和替换来上拉常量。那么如果newProjects == null,则不做任何优化。否则继续向下执行相关优化操作。
代码语言:javascript复制public void onMatch(RelOptRuleCall call) {
final Project project = call.rel(0);
final Filter filter = call.rel(1);
final RelBuilder builder = call.builder();//RelNode构建器
List<RexNode> projects = project.getChildExps();//Returns a list of this relational expression's child expressions.
List<RexNode> newProjects = rewriteProjects(projects, filter.getCondition(), builder);
if (newProjects == null) {//没产生新的Project对象,则不做任何优化
return;
}
RelNode newProjRel = builder.push(filter)
.project(newProjects, project.getRowType().getFieldNames()).build();
call.transformTo(newProjRel);
}
使用rewriteProjects重写Project生成新的Project,使用Builder对象压入Filter对象,再重新构建已上拉了常量的新Project对象,注册到优化器。
3)rewriteProjects方法是常量上拉最为关键的部分,其对Project进行了重写优化并返回一个新Project对象。
使用RelOptUtil.conjunctions将所有谓词表达式拆分为可用AND连接的RexNode列表,这点可参考前期的文章成本模型的部分谓词选择率相关析取范式与合取范式部分,简单来说就是Or和And连接的谓词都可以相互转换。
遍历这些谓词RexNode,判读必须是RexCall对象,RexCall是通过调用运算符op而形成的表达式,其中零个或多个表达式作为操作数。运算符可以是二元的、一元的、函数的、特殊的语法结构,比如 id=1 and address like '%上海%' 中的 等于“=”或like等操作符,id 和 1 为操作数。重写Project的RexCall仅为EQUALS等值操作符和为NULL的IS_NULL的<字段名称,RexNode常量>映射存放到conditions中。使用RexReplacer生成对象对根RelNode和replacer.mutate(newProjects)把谓词Filter常量谓词进行替换。
常量谓词表达式,就如a =1 或 name <> '张三' 一侧带有常量的谓词表达式,此优化Rule仅支持优化等值和为NULL的常量上拉。举例说明:
例如:
SELECT id,name,age FROM student WHERE age = 18 and name <> '张三'
等值常量谓词上拉后为
SELECT id,name,18 as age
FROM student WHERE age = 18 and name <> '张三'
把where条件中可确定的常量age = 18,就18 作为age字段值存放到Select当中。
代码语言:javascript复制private static List<RexNode> rewriteProjects(List<RexNode> projects,
RexNode newPushedCondition, RelBuilder relBuilder) {
final List<RexNode> conjunctions = RelOptUtil.conjunctions(newPushedCondition);//转换为and连接的rexnode
final Map<String, RexNode> conditions = new HashMap<String, RexNode>();//存放操字段名称与常量值映射关系
for (RexNode conjunction: conjunctions) {
// 1.1. If it is not a RexCall, we continue
if (!(conjunction instanceof RexCall)) {
continue;
}
// 1.2. We extract the information that we need
RexCall conjCall = (RexCall) conjunction;
switch (conjCall.getOperator().getKind()) {//谓词中操作符的判断"=,>,>=,<,<=,<>"等,仅仅取得等值和为null常量
case EQUALS:
if (!(RexUtil.isConstant(conjCall.operands.get(0))) && //左侧为字段,右侧为常量,如 id =100
RexUtil.isConstant(conjCall.operands.get(1))) {
conditions.put(conjCall.operands.get(0).toString(), conjCall.operands.get(1));
} else if (!(RexUtil.isConstant(conjCall.operands.get(1))) &&
RexUtil.isConstant(conjCall.operands.get(0))) {//左侧为常量,右侧为字段,如 100 = id
conditions.put(conjCall.operands.get(1).toString(), conjCall.operands.get(0));
}
break;
case IS_NULL:
conditions.put(conjCall.operands.get(0).toString(),
relBuilder.getRexBuilder().makeNullLiteral(//创建一个字面量的
conjCall.operands.get(0).getType().getSqlTypeName()));//把为null的字段存入conditions。
}
}
RexReplacer replacer = new RexReplacer(relBuilder.getRexBuilder(), conditions);
List<RexNode> newProjects = Lists.newArrayList(projects);
replacer.mutate(newProjects);
if (replacer.replaced) {//被替换了,则返回替换后的
return newProjects;
}
return null;
}
其中,IS_NULL的处理部分,在Calcite中字段 is null的NULL作为字面量值返回作为的字段的值。RexReplacer类遍历器功能,RexReplacer 是传递行表达式RexNode,为每个节点调用适合节点类型的处理程序方法。像RexVisitor一样,这是visitor模式的一个实例。但是如果希望方法返回值,请使用RexShuttle。
总结
ProjectFilterPullUpConstantsRule优化规则就是where出现常量等值谓词表达式形如a=1,同时select 含有a字段,那么就确定select中的a字段的为1。就在select中a字段的值,把a=1常量值1上拉到select中,select 1 达到优化目的。