HiveJoinAddNotNullRule
HiveJoinAddNotNullRule:此优化规则Rule主要功能是将SQL语句中Inner Join关联时,出现在关联条件中的字段存在为null可能的字段,都加上相应字段 is not null条件限制。
核心属性和方法:
1)matches方法
matches方法返回此规则Rule是否可能与给定的操作数operands匹配。....
public boolean matches(RelOptRuleCall call) { return true; }
2)onMatch方法
此方法是优化规则对RelNode做优化等价变换的关键。实现了getNotNullConditions方法,把RelNode中所引用的字段的索引列表和字段名称的代表的RexNode行表达式列表中,存在可能为空的字段,都加上IS_NOT_NULL的条件限制,并返回相应的RexNode行表达式列表。这也是此优化规则对RelNode树做变换的关键。
private static List<RexNode> getNotNullConditions(RelOptCluster cluster, RexBuilder rexBuilder, RelNode input, Set<Integer> inputKeyPositions, Set<String> pushedPredicates) { final List<RexNode> newConditions = Lists.newArrayList(); for (int pos : inputKeyPositions) {//遍历输入字段的索引位置,并从行类型的字段列表获取盖RelDataType是否为可空,如果可不空,则跳过 RelDataType keyType = input.getRowType().getFieldList().get(pos).getType(); // Nothing to do if key cannot be null if (!keyType.isNullable()) { continue; } RexNode cond = rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, rexBuilder.makeInputRef(input, pos));//给字段引用添加 is not null限制,生成新的RexNode表达式 String digest = cond.toString(); if (pushedPredicates.add(digest)) {//如果pushedPredicates不存在,则天道新条件行表达式RexNode列表中,返回 newConditions.add(cond); } } return newConditions; }
通过参数inputKeyPositions和pushedPredicates分别为关联条件谓词引用RexNode在schema的索引位置和中文描述列表,通过变换把存在可能为null的字段,添加IS_NOT_NULL限制生成新RexNode,添加到newConditions,作为新的关联条件RexNode列表返回。
虽然此条规则中,matches方法默认是返回ture。但在此onMatch方法中,也可做一些是否满足优化规则条件的判断。
满足此优化条件的如下:
- JOIN关联类型为INNER内关联
- 必须含有关联条件,并ON关联条件不能恒为true,否则就变成笛卡尔积。
首先,获取Join对象,并获取关联左右两侧的输入RelNode对象。并判断关联类型是否为INNER JOIN,否则return不做任何优化。其次,或判断Join对象的关联条件,如果isAlwaysTrue恒为true,这就相当于笛卡尔积了,也不做任何优化。
final Join join = call.rel(0); RelNode lChild = join.getLeft(); RelNode rChild = join.getRight(); HiveRulesRegistry registry = call.getPlanner().getContext().unwrap(HiveRulesRegistry.class); assert registry != null; if (join.getJoinType() != JoinRelType.INNER) {//关联条件不是inner join内连接,将会不会做任何优化 return; } if (join.getCondition().isAlwaysTrue()) {//join的关联条件判断是否一直为true,如果恒为true,类似笛卡尔积,则也不会做任何优化 return; }
获取JoinPredicateInfo关联谓词信息对象,如果出现语义异常,同样返回return结束,不做任何优化。
JoinPredicateInfo joinPredInfo; try { joinPredInfo = HiveCalciteUtil.JoinPredicateInfo.constructJoinPredicateInfo(join); } catch (CalciteSemanticException e) { return; }
JoinPredicateInfo:join谓词信息,表现为Join关联条件时,使用JoinLeafPredicateInfo叶子结点谓词信息来表示谓词中单个关联元素。如:JoinPredicateInfo = JoinLeafPredicateInfo1 and JoinLeafPredicateInfo2。包含如下
- 为等值关联equi-join(equiJoinPredicateElements),保留了关联元素的顺序
- 保存了等值连接join的左右子RelNode的投影索引,这些索引都在join relNode的schema中。
- 保存了join keys的投影索引与连接元素的JoinLeafPredicateInfo映射关系
从上述已获取JoinPredicateInfo对象获取join的等值谓词信息元素在schema中索引信息,左右两侧的分别存入joinLeftKeyPositions和joinRightKeyPositions集合。
Set<Integer> joinLeftKeyPositions = new HashSet<Integer>(); Set<Integer> joinRightKeyPositions = new HashSet<Integer>(); for (int i = 0; i < joinPredInfo.getEquiJoinPredicateElements().size(); i++) { JoinLeafPredicateInfo joinLeafPredInfo = joinPredInfo. getEquiJoinPredicateElements().get(i); joinLeftKeyPositions.addAll(joinLeafPredInfo.getProjsFromLeftPartOfJoinKeysInChildSchema());//提取出使用到谓词在schema中的索引 joinRightKeyPositions.addAll(joinLeafPredInfo.getProjsFromRightPartOfJoinKeysInChildSchema()); }
使用getNotNullConditions分别对左右两侧的谓词引用元素,再分别生成新的不null的条件列表newLeftConditions和newRightConditions。
// Build not null conditions final RelOptCluster cluster = join.getCluster(); final RexBuilder rexBuilder = join.getCluster().getRexBuilder(); Set<String> leftPushedPredicates = Sets.newHashSet(registry.getPushedPredicates(join, 0)); final List<RexNode> newLeftConditions = getNotNullConditions(cluster, rexBuilder, join.getLeft(), joinLeftKeyPositions, leftPushedPredicates); Set<String> rightPushedPredicates = Sets.newHashSet(registry.getPushedPredicates(join, 1)); final List<RexNode> newRightConditions = getNotNullConditions(cluster, rexBuilder, join.getRight(), joinRightKeyPositions, rightPushedPredicates); //在join右侧输入RelNode,根据在schema中索引信息,提取对应索引对应的RexNode表达式,存放到行表达式的List,便于下面使用 // Nothing will be added to the expression RexNode newLeftPredicate = RexUtil.composeConjunction(rexBuilder, newLeftConditions, false); //把所有谓词都用and连接起来 RexNode newRightPredicate = RexUtil.composeConjunction(rexBuilder, newRightConditions, false); if (newLeftPredicate.isAlwaysTrue() && newRightPredicate.isAlwaysTrue()) { return; }
把新生成的条件RexNode列表,用RexUtil.composeConjunction方法用AND连接起来分别为newLeftPredicate和newRightPredicate,整体判断是否问恒为true。如果为真,则不做任何优化。如果都不恒为真,并把新的谓词信息创建Filter并复制到原lChild和rChild对象上。
if (!newLeftPredicate.isAlwaysTrue()) {//如果谓词表达式不恒为true RelNode curr = lChild; lChild = filterFactory.createFilter(lChild, newLeftPredicate);//创建带有谓词的join左侧输入RelNode call.getPlanner().onCopy(curr, lChild);// } if (!newRightPredicate.isAlwaysTrue()) { RelNode curr = rChild; rChild = filterFactory.createFilter(rChild, newRightPredicate); call.getPlanner().onCopy(curr, rChild); }
使用join.copy方法,用关联条件中引用的谓词元素,可能为null的都添加了IS_NOT_NULL判断后新生成的条件,生成新的Join对象newJoin,再把newJoin和谓词信息组册到HiveRulesRegistry对象,此类在整个优化规则使用过程中,起到很作用的作用,主要功能:
- rule规则与relnode关系节点的map映射
- relnode与相关表达式(字符串表示)集合Set
两种关系集合的封装,最后把newJoin注册优化器。
Join newJoin = join.copy(join.getTraitSet(), join.getCondition(), lChild, rChild, join.getJoinType(), join.isSemiJoinDone());//从新创建新Join call.getPlanner().onCopy(join, newJoin);//当关系表达式复制到类似表达式时调用 // Register information about created predicates registry.getPushedPredicates(newJoin, 0).addAll(leftPushedPredicates); registry.getPushedPredicates(newJoin, 1).addAll(rightPushedPredicates); call.transformTo(newJoin);
join.isSemiJoinDone() 判断LogicalJoin 是否已由JoinAddRedundantSemiJoinRule规则优化成了SemiJoin,默认是false。因为hive2.3还没有此条规则。
总结:
Inner join不是支持null值连接的,优化器在生成执行计划时,默默地把引用的可能为null的谓词加上IS_NOT_NULL限制。
HiveJoinCommuteRule
HiveJoinCommuteRule:通过改变Join左右两侧的输入RelNode的顺序来试图探索可优化的执行计划。但前提是对Join关联操作之上Project投影操作的RelNode树
原sql:
SELECT a0,a1,b0,b1 FROM TA JOIN TB ON TA.a0 = TB.b0
等价变换sql:
SELECT a0,a1,b0,b1 ( SELECT b0,b1,a0,a1 FROM TB JOIN TA ON TA.a0 = TB.b0 )
Join物理层算法实现是Nest Loop算法,通过改变了左右两表的顺序,是可以减少IO次数的,IO次数也是影响执行效率的因素之一,同时IO也是CBO基于成本优化器成本模型CostModel元素之一。如果Join物理层算法实现是Hash Join或Sort Merge Join算法改变顺序,将“小的”输入进行hash或进行分桶来减少计算成本。
核心方法和属性:1)matches方法...
2) onMatch方法
首先,用call.rel(0)获取顶层Project投影操作,其次,call.rel(1)获取子输入Join操作。
开始判断Project投影字段的置换topPermutation不为null,则说明它仅仅是输入字段的置换;值为null,则不做任何优化。同样,该字段索引的置换如果为恒等置换,也不做任何优化
Project topProject = call.rel(0); Join join = call.rel(1); final Permutation topPermutation = topProject.getPermutation(); /** * 如果投影Project仅仅是它输入字段的置换。则返回该置换,如果不是,则返回null */ if (topPermutation == null) { //这里说明是单射、满射或双射,而不是一对多非置换了 return; } if (topPermutation.isIdentity()) {//恒等置换,自身到自身的置换 return; }
HiveJoinCommuteRule优化规则没直接使用Calcite的优化规则JoinCommuteRule的逻辑,仅仅只是使用了swap方法把Join左右两侧输入进行调换实现。
如果参数join的输入顺序没有改变则返回null。其次需要一个Project投影保证其字段的顺序,如果没project,将不做任何优化。
获取到改变Join的输入顺序后,对swapped的Project进行,同上的判断,如果返回Project不是输入字段索引的置换,或该字段索引的置换为恒等置换,则不做任何优化。
//To preserve the order of columns in the output row, the rule adds a Project. final RelNode swapped = JoinCommuteRule.swap(join,true); if (swapped == null) {//如join输入顺序没改变,则为null return; } if (swapped instanceof Join) {//如果没Project,而是Join的话,直接退出优化 return; } //转后join输入的顺序后的添加的Project投影操作 final Project bottomProject = (Project) swapped; final Permutation bottomPermutation = bottomProject.getPermutation(); if (bottomPermutation == null) { return; } if (bottomPermutation.isIdentity()) { return; }
JoinCommuteRule.swap(join,true),第二参数为true,说明这里支持外连接输入顺序的交换。
最后,顶层Project投影置换topPermutation与join变换输入顺序在顶层添加的Project投影的置换bottomPermutation的乘积的结果为恒等置换则说明可以做等价变换的优化。bottomProject.getInput(0)移除底部Project投影操作,产生新Join注册到优化器。
总结:
JoinCommuteRule优化规则通过Join的输入顺序来达到优化目标,这是蛮成熟的一条优化规则,Oracle,SQL Server,Mysql都此相应的JoinCommuteRule优化规则。
PartitionPruneRule
PartitionPruneRule:对Predicate谓词中识别出分区字段值谓词列表,直接定位到分区目录读取,而不是从全量数据中过滤相关谓词条件数据,从而避免了不必要IO。熟悉Hive的都知道,Hive表数据是根目录及表名称等多级目录存储在HDFS上的。如表交易明细表transaction_detail按天分区,分区字段为day,分区格式为yyyy-MM-dd
核心属性和方法:
1)matches方法...
2) onMatch方法首先,call.rel(0)获取Filter操作和call.rel(1)获取TableScan操作,再调用perform方法识别并整理出分区字段列表。
@Override public void onMatch(RelOptRuleCall call) { HiveFilter filter = call.rel(0);//获取Filter操作 HiveTableScan tScan = call.rel(1);//获取TableScan perform(call, filter, tScan); }
perform方法能根据所读取的HiveTableScan表中Filter中谓词部分提取出哪些表中字段谓词判断,哪些是分区字段过滤条件,识别到分区字段限制条件后可直接定位到HDFS上目录存储的数据,如transaction_detail/day=2019-11-11/file。而不是读取全表数据后再去过滤谓词条件包括分区字段判断条件浪费大量不必要的IO。
protected void perform(RelOptRuleCall call, Filter filter, HiveTableScan tScan) { RelOptHiveTable hiveTable = (RelOptHiveTable) tScan.getTable();//获取所读取的表 RexNode predicate = filter.getCondition();//获取谓词条件的表达式 Pair<RexNode, RexNode> predicates = PartitionPrune .extractPartitionPredicates(filter.getCluster(), hiveTable, predicate);//提取出谓词中分区字段 RexNode partColExpr = predicates.left;//predicates左侧为分区字段的表达式 hiveTable.computePartitionList(conf, partColExpr, tScan.getPartOrVirtualCols()); }
最后hiveTable.computePartitionList(conf, partColExpr, tScan.getPartOrVirtualCols())是识别分区列谓词条件的关键,先从HiveMeta元数据中判断是否是分区表,谓词中使用的是否的分区列等等判断后,才直接定位到数据在HDFS上目录下数据。
总结:
对于熟悉Hive的童鞋,都知道Where条件后加分区列的限制条件,直接定位到HDFS上的存储目录下,但不知其中真正优化机制,是优化器做了列裁剪优化。
HivePreFilteringRule
HivePreFilteringRule:前置过滤器优化规则或谓词下推优化规则。其主要功能是通过哪些谓词下推到离数据源最近的位置,即提前过滤记录数,减少不必要的数据量IO。大致优化过程,是通过把谓词集合从析取范式(DNF) 和合取范式(CNF)根据需要可相互转换,再确定谓词表达式或函数的确定性或非确定性以及是否可下推的优化。
合取范式(CNF)为AND连接谓词表达式,析取范式(DNF)为OR连接的谓词表达式,并且OR连接谓词表达式和AND连接的表达式可相互转换。合取范式(CNF)即AND连接的谓词表达式,拆分为各个谓词表达式元素集合提取析取范式(DNF)中公共谓词表达式因子。从谓词表达式元素集合在分类为确定性、非确定的和可下推的谓词表达式集合,把可下推谓词进行下推到离数据源头最近的地方,提前减少不必要的数据量。这些都是此HivePreFilteringRule前置过滤器要做的事情。
核心属性和方法:
1)matches方法
matches方法返回此规则Rule是否可能与给定的操作数operands匹配。...判断由RelOptCall调用的优化规则Rule是否与输入参数RelNode关系表达式匹配,即此优化规则Rule能否应用到一个RelNode关系表达式树上。
满足此优化规则条件至少有两条如下:
- 必须形如:Filter - TableScan操作符树
- 此优化规则没有被访问优化过
@Override public boolean matches(RelOptRuleCall call) { final Filter filter = call.rel(0); final RelNode filterChild = call.rel(1); if (filterChild instanceof TableScan) {//如果Filter子输入不是Tablescan则推出优化 return false; } HiveRulesRegistry registry = call.getPlanner().getContext().unwrap(HiveRulesRegistry.class); //如果操作符 已经被rule访问了,我们就不需要在应用优化 if (registry != null && registry.getVisited(this).contains(filter)) { return false; } return true; }
2)onMatch方法
首先,call.rel(0)获取Filter过滤器,也是RelNode关系表达式树的根。
call.getPlanner().getContext().unwrap方法是为库用户提供一种在计划程序会话中,存储数据并在规则中访问数据的方法框架可以实现自己的上下文实现,并将其作为FrameworkConfig的一部分传递。只需实现Wrapper.unwrap(java.lang.Class<C>)方法即可返回您希望提供的任何子对象。这里存储在会话中的会话是HiveRulesRegistry对象。其存储了当前优化规则Rule与访问RelNode映射关系,以免重复访问或变换。
HiveRulesRegistry是两种关系集合的封装:1、当前rule规则与已访问RelNode关系节点的map映射 2、RelNode与所属关系相关表达式(字符串表示)Set集合。registry.registerVisited(this, filter)把当前规则即HivePreFilteringRule访问Filter对象访问记录存储成映射关系。通过从DNF表达式(析取范式 OR)中提取公共元素来重新编译过滤器。
final Filter filter = call.rel(0); //先取第一个操作符 //获取当前库用户会话的HiveRulesRegistry对象 HiveRulesRegistry registry = call.getPlanner().getContext().unwrap(HiveRulesRegistry.class); if (registry != null) { registry.registerVisited(this, filter); //把当前rule与relnode的map, } //获取行表达式的RexNode构建器 final RexBuilder rexBuilder = filter.getCluster().getRexBuilder(); RexNode topFilterCondition = RexUtil.pullFactors(rexBuilder, filter.getCondition());
RexUtil.pullFactors把Ors表达式中相同的因子上拉, 并创建一个等价的RexNode行表达式topFilterCondition。
一个字段有多个值也只有Or连接表达式中出现,一个字段有多个值的谓词判断在And连接是错的。所以才会从or连接中提取公共元素,上拉之后,就变成了AND连接,如:
- (a=1 and b=2)or (a=1 and b= 3) -> a=1 and ( b=2 or b= 3) 能提取出公共因子的情况,变成AND连接
- (a=2 and b=2)or (a=1 and b= 3) -> a=1 and ( b=2 or b= 3) 不能提取出公共因子的情况,还是OR连接
再分别创建可下推的谓词、确定性和非确定性RexNode行表达式集合。一个表达式确定性与非确定性的区别是给定函数同一个确定值,是否永远返回同一个确定值。刚好相反的是非确定性函数,如随机函数Randow()每次返回的值都不确定。
//我们提取可能被下推的候选行表达式RexNode List<RexNode> operandsToPushDown = new ArrayList<>(); //可下推的rexnode List<RexNode> deterministicExprs = new ArrayList<>(); //确定性表达式rexnode List<RexNode> nonDeterministicExprs = new ArrayList<>(); //非确定性表达式rexnode
如果提取完公共元素的RexNode是AND组成,则用RexUtil.flattenAnd把系列AND节点,转换为一个List,null节点作为常量True,提取共同的可以下推谓词表达式。
遍历由上述AND节点,组成的List,步骤:1、如果AND节点内再含有OR表达式,再使用extractCommonOperands对OR表达式提供出公共因子,类似where条件多个or的表达式内公共的限制条件 2、对上述1,OR表达式内含有的公共因子的列表进行遍历 3、并把每个公共因子的描述信息Digest和因子RexNode添加到可能需要下推的集合中 4、如果AND节点的行表达式RexNode是确定的RexNode,则把此RexNode添加到确定的行表达式集合,否则添加到非确定的行表达式集合
switch (topFilterCondition.getKind()) {//Returns the kind of node this is. case AND: //如果公共元素是and组成 //把一系列AND节点,转换为一个List,null节点作为常量True */ ImmutableList<RexNode> operands = RexUtil.flattenAnd(((RexCall) topFilterCondition) .getOperands()); //存放进行下推操作数的描述的集合 Set<String> operandsToPushDownDigest = new HashSet<String>(); List<RexNode> extractedCommonOperands = null; //抽取出公共操作因子集合 for (RexNode operand : operands) {// 遍历and的 RexNode if (operand.getKind() == SqlKind.OR) {//如果有and内,再含or表达式 extractedCommonOperands = extractCommonOperands(rexBuilder, operand, maxCNFNodeCount); //再次提取公共元素 for (RexNode extractedExpr : extractedCommonOperands) { if (operandsToPushDownDigest.add(extractedExpr.toString())) {//如果集合不存在:返回true operandsToPushDown.add(extractedExpr); //添加到可能操作符集合 } } } //分拣出确定表达式和非确定表达式 if (HiveCalciteUtil.isDeterministic(operand)) { deterministicExprs.add(operand); } else { nonDeterministicExprs.add(operand); } } //从非确定性的集合个数大于0, 并遍历确定性的表达式结合 并把其元素RexNode添加到可能下推的集合中 */ if (nonDeterministicExprs.size() > 0) { for (RexNode expr : deterministicExprs) { if (!operandsToPushDownDigest.contains(expr.toString())) { operandsToPushDown.add(expr); operandsToPushDownDigest.add(expr.toString()); } } //即Or表达式中,相同的因子被上拉。 //转换一个表达式集合 转换为and连接,如果有0个表达式,则返回true,如果有一个表达式,则只返回此表达式, //如果表达式任意一个为false,则返回false,移除表达式总是认为是true,如果nullonempty并表达式为true,则返回null topFilterCondition = RexUtil.pullFactors(rexBuilder, RexUtil.composeConjunction(rexBuilder, nonDeterministicExprs, false)); } break; case OR://如果此Kind为Or,则使用extractCommonOperands函数提取公共因子,作为可能下推的集合operandsToPushDown operandsToPushDown = extractCommonOperands(rexBuilder, topFilterCondition, maxCNFNodeCount); break; default: return; } //如果没有找到可下推的 表达式,则跳出 if (operandsToPushDown.isEmpty()) { return; }
上述讲述了topFilterCondition.getKind()是AND连接的情况。那么如果topFilterCondition.getKind()为OR连接的话,直接使用extractCommonOperands提取公用谓词表达式作为可下推的谓词表达式集合对象。HiveCalciteUtil.getPredsNotPushedAlready给定一个谓词可能下推的列表,此方法返回一个需要下推的谓词的集合,返回值:需要谓词下推的集合
需排除以下:
- 已经排除在外的,谓词的String字符串表达形式的集合,不应该包括在内
- 或他们已经是输入节点在子树根节点root的也应该排除在外
然后再次提取公用谓词表达式确定可下推的谓词表达式集合对象,创建新已下推的Filter注册到RelNode等价的RelNode集合。
// 3. If the new conjuncts are already present in the plan, we bail out //如果一个新的and连接,已经在现有的执行计划中,则跳出优化 final List<RexNode> newConjuncts = HiveCalciteUtil.getPredsNotPushedAlready(filter.getInput(), operandsToPushDown); RexNode newPredicate = RexUtil.composeConjunction(rexBuilder, newConjuncts, false); //返回的and连接的行表达式 if (newPredicate.isAlwaysTrue()) { return; } // 4. Otherwise, we create a new condition // 提取Ors当中的共同因子并创建一个新的等价的节点 final RexNode newChildFilterCondition = RexUtil.pullFactors(rexBuilder, newPredicate); // 5. We create the new filter that might be pushed down RelNode newChildFilter = filterFactory.createFilter(filter.getInput(), newChildFilterCondition); RelNode newTopFilter = filterFactory.createFilter(newChildFilter, topFilterCondition); // 6. We register both so we do not fire the rule on them again if (registry != null) { registry.registerVisited(this, newChildFilter); registry.registerVisited(this, newTopFilter); } call.transformTo(newTopFilter);
总结:
通过哪些谓词下推到离数据源最近的位置,即提前过滤记录数,减少不必要的数据量IO
HiveAggregateProjectMergeRule
HiveAggregateProjectMergeRule:主要功能是将Project投影操作之上的Aggregate聚合函数操作两者进行合并,前提是只有当聚合函数的GroupBY分组表达式和参数是字段引用(即,不是表达式)时,才满足优化规则使用条件。如果识别到Project上的Aggregate操作,如果是通过Project做的汇总,进行两者合并或将Project移除,即group by 字段和投影字段相同,将两者合并。在某些情况下,此规则具有修剪的效果:聚合将使用比Projetct投影操作更少的列。
核心属性和方法:
1)matches方法
matches方法返回此规则Rule是否可能与给定的操作数operands匹配。...
判断由RelOptCall调用的优化规则Rule是否与输入参数RelNode关系表达式匹配,即此优化规则Rule能否应用到一个RelNode关系表达式树上。
如果此表达式,含有GroupId,这条规则不能应用,因为GroupId的变化,Value也会发生改变
@Override public boolean matches(RelOptRuleCall call) { final Aggregate aggregate = call.rel(0); //获取root根节点表达式 for (AggregateCall aggCall : aggregate.getAggCallList()) { if (aggCall.getAggregation().equals(HiveGroupingID.INSTANCE)) {//判断是否含有GroupID return false; } } return super.matches(call); }
Group_ID是group_sets集合中分组ID(类似排列组合的分组ID,1组、2组、3组等)。下面例子会使用group_sets和GROUPINGID进行查询,其中的 GROUPINGID,表示结果属于哪一个分组集合。
SELECT month, day, COUNT(DISTINCT cookieid) AS uv, GROUPING__ID FROM tab_test GROUP BY month,day GROUPING SETS (month,day,(month,day)) ORDER BY GROUPING__ID; 结果: month day uv GROUPING__ID ------------------------------------------------ 2015-03 NULL 5 1 2015-04 NULL 6 1 NULL 2015-03-10 4 2 NULL 2015-03-12 1 2 NULL 2015-04-12 2 2 2015-03 2015-03-10 4 3 2015-03 2015-03-12 1 3 2015-04 2015-04-12 2 3 等价于 SELECT month,NULL,COUNT(DISTINCT cookieid) AS uv,1 AS GROUPING__ID FROM tab_test GROUP BY month UNION ALL SELECT NULL,day,COUNT(DISTINCT cookieid) AS uv,2 AS GROUPING__ID FROM tab_test GROUP BY day UNION ALL SELECT month,day,COUNT(DISTINCT cookieid) AS uv,3 AS GROUPING__ID FROM tab_test GROUP BY month,day 说明:grouping sets 只会根据,sets集合内每个元素单独分组:month、day、(month,day)三个分组 注意:group by中字段集合 要 包含 grouping sets()集合字段,否则会报错,即{group by} >={grouping sets}
2)onMatch方法
call.rel(1)获取Project投影操作,call.rel(0)也即获取的Project操作之上Aggregate操作。apply函数将Project投影操作之上的Aggregate聚合函数操作两者进行合并的关键,返回优化后的非空的RelNode,RelOptRuleCall调用转换方法注册到RelSet集合,以备优化器构建最优执行计划。
@Override public void onMatch(RelOptRuleCall call) { final HiveAggregate aggregate = call.rel(0);// 根表达式root expression 为Aggregate final HiveProject project = call.rel(1); //下一个表达式为Project RelNode x = apply(aggregate, project); //两个操作应用到一个RelNode if (x != null) { call.transformTo(x);//调用转换 } }
3)apply方法
该方法涉及到等价变换的具体过程。传入参数为Aggregate操作对象和Project投影操作对象
public static RelNode apply(HiveAggregate aggregate, HiveProject project)
输入的字段是基于0的。如果有多个输入,则它们将连续编号。如果连接的输入是如下:
RexInputRef:(序号,字段数据类型)代表 一个字段 * Input #0: EMP(EMPNO, ENAME, DEPTNO) and * Input #1: DEPT(DEPTNO AS DEPTNO2, DNAME) 字段分别如下: * Field #0: EMPNO * Field #1: ENAME * Field #2: DEPTNO (from EMP) * Field #3: DEPTNO2 (from DEPT) * Field #4: DNAME
因此 RexInputRef(3, Integer) is 字段 DEPTNO2的正确的引用.
- 初始化groupset字段索引与投影中字段索引的映射关系,并判断Project投影的行表达式,是一个字段的引用,而不是函数表达式,否则将无法应用此优化。
for (int key : aggregate.getGroupSet()) {//Returns a bit set of the grouping fields ( 如上述:grouping sets(cur_stt,crt_tim) ) final RexNode rex = project.getProjects().get(key); //project.getProjects()返回类型:List<RexNode> //select 1,2,sum(a) from t group by 1,2 if (rex instanceof RexInputRef) { //判断Project投影的行表达式,是一个字段的引用,而不是函数之类的 final int newKey = ((RexInputRef) rex).getIndex(); //取出字段引用的Ref的字段序号。 newKeys.add(newKey); map.put(key, newKey); //初始化groupset字段索引与投影中字段索引的映射关系 } else { // Cannot handle "GROUP BY expression" return null; } }
2 .遍历调用汇总函数,函数列表,判断AGG引用的字段是否在Project投影中引用,而且是字段引用,而不是表达式的引用,否则将跳出优化。
for (AggregateCall aggregateCall : aggregate.getAggCallList()) {//遍历调用汇总函数,函数列表 final ImmutableList.Builder<Integer> newArgs = ImmutableList.builder(); for (int arg : aggregateCall.getArgList()) {//遍历 每个汇总函数内的参数,并到投影中确认,判断是否引用到字段,并添加到newArgs列表中,否则返回为null final RexNode rex = project.getProjects().get(arg); // 如果在Project投影中,没有找到则返回null或返回的不是字段引用,最终结果返回null,则会跳出优化 if (rex instanceof RexInputRef) { newArgs.add(((RexInputRef) rex).getIndex()); } else { // Cannot handle "AGG(expression)" return null; } }
3. 如果groupset顺序不同,或者包含重复,则添加一个Project。判断这两个列表是否相等,如果不相等,则进行遍历newKeys索引,并查找对应newGroupSet索引位置,添加到postList中。使用new Aggregate和posList列表创建一个new Project投影。这里完成了Aggregate和Project合并的操作作为一个RelNode。
RelNode rel = newAggregate; if (!newKeys.equals(newGroupSet.asList())) { //判断这两个列表是否相等,如果不相等,则进行遍历newKeys索引,并查找对应newGroupSet索引位置,添加到postList中。 final List<Integer> posList = Lists.newArrayList(); for (int newKey : newKeys) { posList.add(newGroupSet.indexOf(newKey)); } if (aggregate.indicator) {//如果indicator为true for (int newKey : newKeys) { posList.add(aggregate.getGroupCount() + newGroupSet.indexOf(newKey));//在分组字段个数的基础上+索引 } } for (int i = newAggregate.getGroupCount() + newAggregate.getIndicatorCount(); i < newAggregate.getRowType().getFieldCount(); i++) { posList.add(i); } rel = HiveRelOptUtil.createProject( HiveRelFactories.HIVE_BUILDER.create(aggregate.getCluster(), null), rel, posList);// 这里合并最要的一步:使用new Aggregate和posList列表创建一个new Project投影。这里完成了Aggregate和Project合并的操作作为一个RelNode。 } return rel; }
总结:
优化规则HiveAggregateProjectMergeRule是将Project投影和Aggregate汇总参数及GroupBy引用字段(注,不能是表达式)相同,会将Aggregate和Project进行合并。