Hive优化器原理与源码解析系列--优化规则AggregateProjectPullUpConstantsRule(十七)

2022-04-25 15:39:03 浏览数 (3)

目录

背景

优化规则AggregateProjectPullUpConstantsRule

  • matches方法逻辑详解
  • onMatch方法逻辑详解

总结

背景

这篇文章来讲优化规则AggregateProjectPullUpConstantsRule,顾名思义是将Aggregate汇总操作中常量字段上拉到Project投影操作中的优化规则,主要功能从Aggregate聚合中删除常量键。常量字段是使用RelMetadataQuery.getpulldupredicates(RelNode)推导的,其输入不一定必须是Project投影操作。但此Rule规则从不删除最后一列,简单来讲,如果groupBy字段只有一列,而且为常量,也不会执行此优化,因为聚合Aggregate([])返回1行,即使其输入为空。由于转换后的关系表达式必须与原始关系表达式匹配,为等价变换,因此常量被放置在简化聚合Aggregate上方的Project投影中。

举例说明:

如员工信息表:EMPLOYEE

id ID标识

name 姓名

sex 性别(f:女性 m:男性)

city 城市

待优化前SQL语句:

SELECT

city,

sex,

COUNT(id)

FROM EMPLOYEE

WHERE sex = 'f'

GROUP BY city,sex

通过AggregateProjectPullUpConstantsRule优化规则等价变换后,优化后SQL语句:

SELECT

city,

'f' as sex,

emp_cnt

(

SELECT

city,

COUNT(id) as emp_cnt

FROM EMPLOYEE

WHERE sex = 'f'

GROUP BY city

)

通过从等值谓词中识别GroupBy所引用sex字段值一直为常量'f',于是把Aggregate聚合中GroupBy中sex分组字段移除,在Aggregate操作之上创建一个Project投影,并把GroupBy删除sex常量'f',放置其中,这样就完成了Aggregate操作中常量上拉。

上述这些操作AggregateProjectPullUpConstantsRule优化规则是如何做到的,应用此条规则需要满足哪些条件,接下来详细讲解。

优化规则AggregateProjectPullUpConstantsRule

1)matches方法逻辑详解

matches方法返回此规则Rule是否可能与给定的操作数operands匹配,但是此方法的任何实现都可以给出误报,也就是说虽然规则与操作数匹配,但随后OnMatch(ReloptRuleCall)而不生成任何后续任务。

判断由RelOptCall调用的优化规则Rule是否与输入参数RelNode关系表达式匹配,即此优化规则Rule能否应用到一个RelNode关系表达式树上。但此matches方法是继承自父类方法,默认返回true。

代码语言:javascript复制
public boolean matches(RelOptRuleCall call) {
  return true;
}

2)onMatch方法逻辑详解

接收有关一条规则匹配的通知。同时此方法被调用,call.rels保存了与规则Rule的操作数Operands匹配上的关系表达式RelNode集合;call.rels[0]是根表达式。通常一条规则Rule会检查这些节点是否有效匹配,创建一个新表达式RelNode(等价的)然后调用RelOptRuleCall.transformTo(org.apache.calcite.rel.RelNode, java.util.Map<org.apache.calcite.rel.RelNode, org.apache.calcite.rel.RelNode>)注册表达式。而RelOptRuleCall用一系列RelNode关系表达式集合作为参数,对RelOptRule优化规则的调用。

首先call.rel(0)获取Aggregate操作对象,并取得groupBy引用字段的个数,如果只有GroupBy只有一个字段,已经没有优化的空间,不可能把一个非空groupby转换为空groupBy,即不可能移除仅有一个常量字段。

代码语言:javascript复制
final Aggregate aggregate = call.rel(0);
final RelNode input = call.rel(1);
final int groupCount = aggregate.getGroupCount();//返回groupBy 字段的个数  
if (groupCount == 1) {//如果groupBy仅引用一个字段,则退出优化
    return;
}

哪些是常量字段是RelMetadataQuery.getpulldupredicates(RelNode)提取出关于此输入RelNode的谓词,返回RelOptPredicateList对象推导的。

RelOptPredicateList:

已知保存在特定关系表达式输出中的谓词。

  • 上拉谓词:(字段pulldupredicates是应用于关系表达式输出的每一行的谓词。它们是从输入关系表达式和关系运算符推断出来的。

例如,如果将Filter(x>1)应用于谓词y<10的关系表达式,则过滤器的上拉谓词为[y<10,x>1]。

  • 推断谓词:仅适用于联接。如果联接的左输入上有谓词,并且该谓词位于联接条件中使用的列上,则可以在联接的右输入上推断谓词。(反之亦然。)

如:

SELECT *FROM empJOIN dept ON emp.deptno = dept.deptno

WHERE emp.gender = 'F' AND emp.deptno < 10

说明:

  • 左侧: Filter(Scan(EMP), deptno < 10, predicates: [deptno < 10]
  • 右侧: Scan(DEPT), predicates: []关联Join: Join(left, right, emp.deptno = dept.deptno, leftInferredPredicates: [],
  • 右侧推断谓词rightInferredPredicates: [deptno < 10],
  • 上拉谓词pulledUpPredicates: [emp.gender = 'F', emp.deptno < 10, emp.deptno = dept.deptno, dept.deptno < 10]

注意:来自左输入的谓词出现在rightInferredPredicates中。来自多个源的谓词出现在pulledUpPredicates中。

那么RelOptPredicateList对象的表现形式:[emp.gender = 'F', emp.deptno < 10, emp.deptno = dept.deptno, dept.deptno < 10],如果没有从此RelNode提取的谓词为null,则优化无法继续。

ReduceExpressionsRule.predicateConstants方法把RelOptPredicateList对象提取出等值谓词表达式,如上述的emp.gender = 'F'。以<emp.gender,'F'>形式映射存放在变量constants。遍历GroupBy引用字段的索引,并包装成RexInputRef(序号,字段数据类型)代表一个字段。如果在常量等值谓词映射关系中存在的。则以<字段索引,常量值>映射关系存在,如上述的<字段序号,'F'>。

同样,如果GroupBy后没引用常量字段或引用常量字段没有在等值常量谓词中出现,则推出优化。

代码语言:javascript复制
final RexBuilder rexBuilder = aggregate.getCluster().getRexBuilder();
final RelMetadataQuery mq = RelMetadataQuery.instance();
final RelOptPredicateList predicates =
        mq.getPulledUpPredicates(aggregate.getInput());//参数为聚合的子RelNode
if (predicates == null) {//如果没有提取出谓词predicate,则推出优化
    return;
}
final ImmutableMap<RexNode, RexNode> constants =
        ReduceExpressionsRule.predicateConstants(RexNode.class, rexBuilder,
                predicates);
final NavigableMap<Integer, RexNode> map = new TreeMap<>();
for (int key : aggregate.getGroupSet()) {//遍历GroupBy后的字段的序号(index)
    final RexInputRef ref =
            rexBuilder.makeInputRef(aggregate.getInput(), key);//包装成RexInputRef(序号,字段数据类型)代表 一个字段
    if (constants.containsKey(ref)) {//判断是否存在
        map.put(key, constants.get(ref));
    }
}
if (map.isEmpty()) { //如果groupBy引用的字段,都不是常量,则退出优化
    return;
}
if (groupCount == map.size()) {
     //如果groupBy个数全是常量项的话,则删除。不能全部上拉
    map.remove(map.navigableKeySet().first());
}

最后, 如果groupBy个数全是常量项的话,则删除。但“分组依据”中至少需要一个项目。否则,“GROUP BY 1,2”可能会更改为“GROUP BY()”。移除第一个元素在这里不是最优的,不过,它将允许我们使用下面的快速路径(只需修剪groupCount)。

创建上拉的Aggregate聚合操作,移除聚合中使用的常量。

遍历aggregate.getGroupSet()返回对象GroupBy字段的位图索引,判断如果在常量map中存在,则删除。这也是删除GroupBy常量的关键部分(哪些常量是可以删除,仔细看前面讲过的,生成删除后的新newGroupSet。创建删除常量后的新Aggregate对象。

代码语言:javascript复制
ImmutableBitSet newGroupSet = aggregate.getGroupSet();//Returns a bit set of the grouping fields.
for (int key : map.keySet()) {
    newGroupSet = newGroupSet.clear(key); //清除GroupBy中引用的常量字段,生成新的newGroupSet对象
}
final int newGroupCount = newGroupSet.cardinality();
//如果常量在组列表的后端,我们只需减少组计数。后面默认舍掉
final RelBuilder relBuilder = call.builder();
relBuilder.push(input);
// Clone aggregate calls.
final List<AggregateCall> newAggCalls = new ArrayList<>();
for (AggregateCall aggCall : aggregate.getAggCallList()) {
    newAggCalls.add(
            aggCall.adaptTo(input, aggCall.getArgList(), aggCall.filterArg,
                    groupCount, newGroupCount));
}
relBuilder.aggregate(
        relBuilder.groupKey(newGroupSet, false, null),
        newAggCalls);//创建删除了GroupBy常量的汇总aggregate

AggregateCall:在Aggregate聚合操作中聚合方法的调用

adaptTo()方法:创建一个等效的AggregateCall,它适用于新的输入类型和/或GROUP BY中的列数。

将上面GroupBy中移除后的常量,放置在新创建的Project投影。

遍历aggregate引用的所有字段列表(包括聚合方法内的字段),如果是聚合方法表达式,名称和位置不变,如果是常量则直接提取出常量值,如'F' 作为字段值放置到Project中。其他依次递增放置到以添加到Pair<字段表达式,字段名称>列表中。

代码语言:javascript复制
// Create a projection back again.
List<Pair<RexNode, String>> projects = new ArrayList<>();
int source = 0;
for (RelDataTypeField field : aggregate.getRowType().getFieldList()) {//遍历聚合的字段列表
    RexNode expr;
    final int i = field.getIndex();
    if (i >= groupCount) { //聚合中的使用字段,不是GroupBy中的字段,则名称和位置不变
        // Aggregate expressions' names and positions are unchanged.
        expr = relBuilder.field(i - map.size());
    } else if (map.containsKey(i)) {//如果此字段 是常量字段,则把此字段放置到Project中。
        expr = map.get(i);//从常量映射中,取出常量值,放置到Project中
    } else {
        expr = relBuilder.field(source);//否则,依次创建聚合表达式
          source;
    }
    projects.add(Pair.of(expr, field.getName()));//添加到Pair<字段表达式,字段名称>
}
relBuilder.project(Pair.left(projects), Pair.right(projects)); // 以字段名称,字段表达式创建Project投影操作
call.transformTo(relBuilder.build());

代码最后部分,以<字段名称,字段表达式>创建Project投影操作,做等价变换注册到RelSet等价的关系表达式集合,已备优化器选择。

总结

优化规则AggregateProjectPullUpConstantsRule将等值谓词常量中出现的,并在GroupBy中引用的字段进行删除,为了保证其等价变换再上拉到Project投影中,减少中间分组计算的过程。

0 人点赞