SortLimitPullUpConstantsRule
SortLimitPullUpConstantsRule:将带有Order by 、 Where等值谓词常量条件的这种SQL语句写法中将谓词中上拉常量到Project投影(Select操作)中。
举例:从student表里取age=18的,并按照postCode邮编排序返回前1000学生信息
原sql: SELECT * FROM ( SELECT id,name,age,postCode FROM student WHERE age = 18 ORDER BY postCode LIMIT 1000 ) PSP; 变换后: SELECT id,name,18,postCode FROM ( SELECT id,name,postCode FROM student WHERE age = 18 ORDER BY PostCode LIMIT 1000 ) PSP;
核心方法和属性:
1)matches方法
此规则Rule只继承了父类的实现,默认返回true。意味着各种ReNode关系表达式都有匹配应用的可能。
matches方法返回此规则Rule是否可能与给定的操作数operands匹配,但是此方法的任何实现都可以给出误报,也就是说虽然规则与操作数匹配,但随后具OnMatch(ReloptRuleCall)而不生成任何后续任务。
2)onMatch方法
接收有关一条规则匹配的通知。同时此方法被调用,call.rels保存了与规则Rule的操作数Operands匹配上的关系表达式RelNode集合onMatch的判断条件如下:(a). RelNode关系表达式的Root根不能是Sort操作符,如图1SQL对Sort操作符再嵌套一层的写法 (b). 如果Project输入字段仅有一个,则不做任何优化,因为没有优化的空间。(c). 有关保留在从关系表达式RelNode发出的行中的谓词的元数据。如果谓词为null,则不做任何优化 (d). 如果谓词表达式中没有常量谓词,则不做任何优化。
HiveReduceExpressionsRule.predicateConstants方法,该方法功能是从上述谓词元数据predicates提取等值的常量谓词,如a=1就是等值常量谓词,name<>'李四'是非等值常量谓词。还如a=1 and a=4 存在不一致问题也不是。把等值常量谓词的结果存放到constants映射(字段表达式,常量表达式)中。
遍历Sort的子Project中的字段引用,对这些字段引用进行分类topChildExprs和newChildExprs。topChildExprs收集这些字段引用RexNode,做顶层Project使用,也是常量上拉到Project的关键。
- 如果此字段在等值常量谓词引用过,则存放常量RexNode。
- 如果此字段在等值常量谓词没引用过,则存放该字段RexNode
Mappings.TargetMapping mapping为将源列映射到目标列的映射关系,目标列与源列是1:N的关系,每个目标列至少对应一个源列,一个源列只能对应一个目标列。inverse()方法是把从源列到目标列的映射关系,翻转为从目标列到源列的映射关系。这样就变成了Project中的所有字段到不在常量谓词中的字段的映射mapping。
RexUtil.apply(mapping, topChildExprs)更新topChildExprs,把常量字段进行了上拉。列表集合准备topChildExprs列表由[ id,name,age,postCode] 替换成了topChildExprs [ id,name,18,postCode],字段a替换成了常量18。
// Update top Project positions select中字段已经更新为常量 topChildExprs = ImmutableList.copyOf(RexUtil.apply(mapping, topChildExprs));//并生成一个新的排序列表
生成RelBuilder构建器来构建RelNode操作符树。
使用newChildExprs非等值常量谓词引用的RexNode列表构建Project。如上述 SELECT id,name,postCode from student where age = 18,注意去掉了age字段,因为age字段是等值常量谓词中引用了。
final RelBuilder relBuilder = call.builder(); relBuilder.push(sort.getInput());//压入的Sort的子RelNode, relBuilder.project(Pair.left(newChildExprs), Pair.right(newChildExprs));//Creates a Project of the given list of expressions and field names.
下面是关键部分,是构建Sort并创建顶层Project将常量从上述去掉age字段的常量上拉到Project中
final ImmutableList<RexNode> sortFields = relBuilder.fields(RelCollations.of(fieldCollations)); relBuilder.sortLimit(sort.offset == null ? -1 : RexLiteral.intValue(sort.offset), sort.fetch == null ? -1 : RexLiteral.intValue(sort.fetch), sortFields); // 使用上述整理的Sort排序字段,创建Sort // Create top Project fixing nullability of fields relBuilder.project(topChildExprs, topChildExprsFields);//创建上拉了常量谓词表达式Project relBuilder.convert(sort.getRowType(), false);//创建将当前关系表达式的输出转换为所需行类型的投影Project。 List<RelNode> inputs = new ArrayList<>(); for (RelNode child : parent.getInputs()) { //最外层的RelNode if (!((HepRelVertex) child).getCurrentRel().equals(sort)) {//遍历父RelNode的子RelNode,不等于的都加入RelNode list,否则使用relBuilder.build() inputs.add(child); } else { inputs.add(relBuilder.build());//Returns the final relational expression. 如果等于sort对象,就返回最终的RelNode } } call.transformTo(parent.copy(parent.getTraitSet(), inputs)); }
总结:
优化规则SortLimitPullUpConstantsRule,需要满足上述几种优化条件后,将Sort子RelNode中Filter等值常量谓词表达式中的字段,替换为常量,上拉到Project中来达到优化的目的的优化Rule。
UnionPullUpConstantsRule
UnionPullUpConstantsRule:将带有UNION ALL、 Where等值谓词常量条件的这种SQL语句写法中将谓词中上拉常量到Project投影(Select操作)中。
原sql:
SELECT key, value, ds FROM ( SELECT key, value, ds FROM src_union_1 UNION ALL SELECT key, value, ds FROM src_union_2 UNION ALL SELECT key, value, ds FROM src_union_3 ) subq where key = 86;
等价变换后sql:
SELECT 86 as key, value, ds FROM ( SELECT value, ds FROM src_union_1 UNION ALL SELECT value, ds FROM src_union_2 UNION ALL SELECT value, ds FROM src_union_3 ) subq where key = 86;
核心属性和方法:
1)matches方法:
matches方法返回此规则Rule是否可能与给定的操作数operands匹配,但是此方法的任何实现都可以给出误报,也就是说虽然规则与操作数匹配,但随后具OnMatch(ReloptRuleCall)而不生成任何后续任务。2)onMatch方法接收有关一条规则匹配的通知。同时此方法被调用,call.rels保存了与规则Rule的操作数Operands匹配上的关系表达式RelNode集合
onMatch的判断条件如下:
a、如果Project输入字段仅有一个,则不做任何优化,因为没有优化的空间。因为我们无法转换为空的Project运算符,如select a from t where a = 1 无法在对等值常量a进行上拉。
b、有关保留在从关系表达式RelNode发出的行中的谓词的元数据。如果谓词为null,则不做任何优化
c、如果谓词表达式中没有常量谓词,则不做任何优化。
总结:
常量上拉的大致思路是对出现在谓词中等于某个常量constant的又出现在Project投影中的变量或列引用,是此列引用不在参与中间结果的一系列的计算,直接在投影Project使用常量作为此列引用的返回值。详细可参考以往文章关于常量上拉优化规则Rule
ProjectOverIntersectRemoveRule
ProjectOverIntersectRemoveRule:把操作符树中INTERSECT交集操作符的之上的Project投影操作符,在满足一定条件下,把Project投影操作符移除减少执行计划的执行成本。
原sql:
SELECT key, value, ds FROM ( SELECT key, value, ds FROM src_intersect_1 INTERSECT SELECT key, value, ds FROM src_intersect_2 INTERSECT SELECT key, value, ds FROM src_intersect_3 ) subq ;
等价变换后sql:
SELECT key, value, ds FROM src_intersect_1 INTERSECT SELECT key, value, ds FROM src_intersect_2 INTERSECT SELECT key, value, ds FROM src_intersect_3;
核心属性和方法:
1)matches方法
public boolean matches(RelOptRuleCall call) { Project project = call.rel(0); Intersect intersect = call.rel(1); return isTrivial(project, intersect); //判断project和intersect字段个数和数据类型是否完全一致。返回true,完全一致。可以移除 } private static boolean isTrivial(Project project, Intersect intersect) { return RexUtil.isIdentity(project.getProjects(), intersect.getRowType()); } public static boolean isIdentity(List<? extends RexNode> exps, RelDataType inputRowType) { return inputRowType.getFieldCount() == exps.size() && containIdentity(exps, inputRowType, Litmus.IGNORE); }
call.rel(0)表示为顶层为Project投影,call.rel(1)表达为顶部的Intersect交集,isTrivial函数是判断project和intersect是否完全一致,包含字段个数和字段的数据类型返回boolean值。isTrivial方法实现,是比较Project和Intersect操作符的的字段,RexUtil.isIdentity返回表达式列表是否投影传入字段
RexUtil.isIdentity方法源码实现:
比较了行表达式列表大小和字段的个数,并且用containIdentity方法内遍历了RexNode行表达式列表元素和RelDataType行数据类型的每个元素数据类型。Litmus为当有效性测试成功或失败时调用的回调。Litmus.IGNORE选择的是忽略。
2)onMatch方法
接收有关一条规则匹配的通知。同时此方法被调用。
public void onMatch(RelOptRuleCall call) { call.transformTo(call.rel(1)); } //如果Project和Intersect字段相同,则移除Project
此条优化规则的onMatch方法设计相对简单,满足了matches判断后,直接跳过表示为顶层的Project投影call.rel(0),直接把call.rel(1) Intersect操作符注册到等价集合。这样意味着把Project进行移除。
总结:
优化规则ProjectOverIntersectRemoveRule相对比较简单,简单的matches方法判断满足顶部为Project投影操作符,底部为Intersect交集操作符,并两者的字段个数和数据类型完全一致,使用call.transformTo(call.rel(1))跳过顶部Project投影,把Intersect交集操作符注册到等价集合,达到Project投影移除来进行优化
ProjectSortTransposeRule
ProjectSortTransposeRule:Project投影操作(相当于HSQL中的Select操作)和Sort排序的调换顺序的优化规则。
核心方法和属性:
1)matches方法
matches方法返回此规则Rule是否可能与给定的操作数operands匹配。....2)onMatch方法
首先使用RelOptRuleCall对象rel(0)方法获取根RelNode关系表达式Project,其次获取Project的子RelNode关系表达式Sort。
final HiveProject project = call.rel(0); final HiveSortLimit sort = call.rel(1);
这里使用来确定Project投影的输入和输出字段之间的映射。如果Project的字段不是 Sort投影内输入和输出字段映射内,即是由表达式expression产生的,非来自Sort的相关字段,则不做任何优化的事情return;。就是所谓matches方法的误报。
// Determine mapping between project input and output fields. If sort // relies on non-trivial expressions, we can't push. final Mappings.TargetMapping map = RelOptUtil.permutation( project.getProjects(), project.getInput().getRowType()).inverse(); for (RelFieldCollation fc : sort.getCollation().getFieldCollations()) { if (map.getTarget(fc.getFieldIndex()) < 0) {//说明映射关系中,输入含有表达式和输出字段的映射。 return; } }
RelOptUtil.permutation方法返回描述输出字段来源的排列,即输入字段和输出字段的映射。在返回的映射中,如果Sort字段输入i投影输出字段n,则map.getTargetOpt(i)的值为n;如果是表达式expression,则为-1。如果返回值为-1,说明输入字段和输出字段之间的映射的不是完全的字段与字段的对应映射,而是含有表达式expression与字段的映射。这里不做任何优化的事情。
顶层Project 和 子Sort这种RelNode表达式树,子Sort作为顶层Project的输入Input,则Project作为Output。两者顺序颠倒,就是Project操作作为子输入Input,而Sort就是作为顶层输出Output。如果子Sort中含有表达式expression,这种过程是不可逆的。例如Sort input输入字段 A + B 对应Project Output输出字段D,这样就导致无法简单的Project和Sort进行顺序颠倒。所以onMatch对这种情况是不做任何优化的。3) permutation方法创建PARTIAL_FUNCTION类型,即每个源最多有一个目标输入和输出之间的映射关系mapping对象。使用Ord.zip方法把project.getProjects()的List<RexNode>生成java.util.List<Ord<E>>对象包含(在行中序号和RexNOde的对应关系),并遍历之把输入字段序号和输出字段序号的对应关系形成映射关系写入到mapping对象。其中如果是简单的Cast函数的类型转换就第一个操作数,如Cast(id as string)取id字段,如果是RexInputRef输入字段引用,则映射关系同样写入mapping对象。最终生成一个输入字段和输出字段的序号映射关系对象。
4) Mapping接口:
Mapping接口是源域到目标域之间的关系。即源字段序号和目标字段序号的之间映射关系。
此接口表示最一般的可能映射。根据特定映射的映射类型,某些操作可能不适用。如果调用该方法,将收到运行时错误。
- 如果目标有多个源,那么方法mappings.sourcemapping.getsource(int)将抛出mappings.toomanyelementexception。
- 如果源没有目标,那么方法mappings.functionmapping.getTarget(int)将抛出mappings.noelementexception。
MappingType类型:
描述映射的类型,从最一般的MULTI_FUNCTION函数(源域和目标域中的每个元素都可以参与许多映射)到最严格的双映射(源域和目标域中的每个元素都必须与另一个域中的一个元素精确配对)。
一些常见类型:
- ONTO类型:每个目标至少有一个源,则满射是一个映射
- PARTIAL_FUNCTION类型:每个源最多有一个目标
- FUNCTION类型:每个源正好有一个目标
- INJECTION类型:注入是一种映射,其中目标最多有一个源;因此有点混乱地称为一对一映射。
- BIJECTION:双映射是一种既注入又回注的映射。每个源都有一个目标,反之亦然。
一旦知道所需的映射类型,就调用mappings.create(mapping type,int,int)来创建该映射的有效实现。
/** * Returns a permutation describing where output fields come from. In * the returned map, value of {@code map.getTargetOpt(i)} is {@code n} if * field {@code i} projects input field {@code n}, -1 if it is an * expression. */ public static Mappings.TargetMapping permutation( List<RexNode> nodes, RelDataType inputRowType) { final Mappings.TargetMapping mapping = Mappings.create( MappingType.PARTIAL_FUNCTION,//如果每个源最多有一个目标,则映射是一个PARTIAL_FUNCTION类型。 nodes.size(),//行表达式RexMode列表的大小。 inputRowType.getFieldCount()//输入行类型的字段的个数 ); for (Ord<RexNode> node : Ord.zip(nodes)) {//引用字段序号和字段表达式之间的映射关系遍历 if (node.e instanceof RexInputRef) { mapping.set( node.i, ((RexInputRef) node.e).getIndex()); } else if (node.e.isA(SqlKind.CAST)) {//如是cast类型转换函数,则取第一个操作数 RexNode operand = ((RexCall) node.e).getOperands().get(0); if (operand instanceof RexInputRef) { mapping.set( node.i, ((RexInputRef) operand).getIndex()); } } } return mapping; }
Ord类型:继承子Map实体的类,用于存放RexNode行表达式和在行中所在的序号。
public class Ord<E> extends java.lang.Object implements java.util.Map.Entry<java.lang.Integer,E> Pair of an element and an ordinal.
zip函数生成一个Ord列表对象。
static <E> java.util.List<Ord<E>> zip(E[] elements) Returns a numbered list based on an array. //创建Ord对象 static <E> Ord<E> of(int n, E e) Creates an Ord.
使用mapping对象和sort的RelCollation生成新的RelCollation排序信息。生成新Project,再使用新的Project生成新的Sort,相当于Project和Sort颠倒顺序。
// Create new collation final RelCollation newCollation = RelCollationTraitDef.INSTANCE.canonize( RexUtil.apply(map, sort.getCollation()));//Applies a mapping to a collation // New operators final RelNode newProject = project.copy(sort.getInput().getTraitSet(), ImmutableList.<RelNode>of(sort.getInput())); final HiveSortLimit newSort = sort.copy(newProject.getTraitSet(), newProject, newCollation, sort.offset, sort.fetch);//使Project做为子RelNode生成Sort,做两者顺序颠倒的变换 call.transformTo(newSort);//注册 }
call.transformTo(newSort)做等价变换后注册到优化器。
总结:
内置的规则中也不是每条优化规则Rule都能保证对此RelNoe关系表达树式进行等价变换后都能减少成本Cost。每次等价交换后注册到RelNode等价关系表达RelNode集合中,由CBO通过计算成本模型CostModel和统计信息来计算成本,从选择最优的执行计划。
HiveProjectMergeRule
HiveProjectMergeRule:顶层Project投影操作(相当于HSQL中的Select操作)和底部Project投影操作进行合并的优化规则,但前提是这些Project不投影相同的输入引用集。
核心方法和属性:
1)matches方法
matches方法返回此规则Rule是否可能与给定的操作数operands匹配。...当前Project投影操作中不支持一个窗口函数与另一个窗口函数的合并。即顶部Project投影操作中RexNode行表达式的序号位,对应与底部Project的相应的序号RexNode行表达式都是窗口函数,则matches返回false。RexOver继承RexCall表达式用来调用窗口Window函数的实现类,如果一个RexNode表达式是RexOver实例,则说明RexNode行表达式为窗口函数。
public class RexOver extends RexCall Call to an aggregate function over a window. public boolean matches(RelOptRuleCall call) { //当前投影合并,不支持窗口函数 final Project topProject = call.rel(0);//顶部Project操作 final Project bottomProject = call.rel(1);//底部Project操作 for (RexNode expr : topProject.getChildExps()) {//遍历顶部Project的输入行表达式 if (expr instanceof RexOver) { Set<Integer> positions = HiveCalciteUtil.getInputRefs(expr);//返回当前字段或行表达式中索引的位置 for (int pos : positions) {//顶Project中相应字段对应的位置来查找在底部Project投影中到行表达式,判读是否为窗口函数 if (bottomProject.getChildExps().get(pos) instanceof RexOver) { return false; } } } } return super.matches(call); }
总之,顶层Project中一个窗口函数中的字段来自底部Project中窗口函数中的字段,是不支持的。
2)onMatch方法
首先使用RelOptRuleCall对象rel(0)方法获取根RelNode关系表达式Project topProject,其次获取Project的子RelNode关系表达式bottomProject。
final Project topProject = call.rel(0); final Project bottomProject = call.rel(1); final RelBuilder relBuilder = call.builder();
虽然在matches方法内优化规则Rule能否应用做了判断,但不会完全覆盖到。onMatch方法内也可做相应的判断,如果不满足条件也不会做任何优化变换而结束。
然后分别获取的顶部和底部的Project投影操作的Permutation对象。如果对象非空并是isIdentity为true,不再做任何优化return结束。
如果上述条件都不满足的话,则通过topPermutation.product(bottomPermutation)方法,通过分析序列化输出创建新的Permutation对象的字段Mapping(可参考HiveProjectMergeRule何为Mapping),取出Permutation对象的字段和相应的数据类型,再使用上面push的底部Project操作子输入RelNode,然后构建器出新合并生成的Project投影操作等价变换后,注册等价关系表达式集合RelSet。
final Permutation topPermutation = topProject.getPermutation(); if (topPermutation != null) { if (topPermutation.isIdentity()) { // Let ProjectRemoveRule handle this. return; } final Permutation bottomPermutation = bottomProject.getPermutation(); if (bottomPermutation != null) { if (bottomPermutation.isIdentity()) { // Let ProjectRemoveRule handle this. return; } //通过分析序列化输出创建Project final Permutation product = topPermutation.product(bottomPermutation); relBuilder.push(bottomProject.getInput()); relBuilder.project(relBuilder.fields(product), topProject.getRowType().getFieldNames()); call.transformTo(relBuilder.build()); return; } }
以下是继续对一棵关系表达式树是否满足匹配优化的条件判断。
if (!force) { if (RexUtil.isIdentity(topProject.getProjects(), topProject.getInput().getRowType())) { return; } }
force是判断Project合并是否为强制模式,如果force=true即强制模式,即使顶层Project和底部Project完全相同,也会执行合并动作。RexUtil.isIdentity方法是判断两个表达式集合的个数和数据类型是否完全一致。
RelOptUtil.pushPastProject方法把顶部Project投影内RexNode行表达式和底部Project投影内RexNode行表达式进行合并成新的Project对象。
force是判断Project合并是否为强制模式,如果force=true即强制模式,即使顶层Project和底部Project完全相同,也会执行合并动作。RexUtil.isIdentity方法是判断两个表达式集合的个数和数据类型是否完全一致。 RelOptUtil.pushPastProject方法把顶部Project投影内RexNode行表达式和底部Project投影内RexNode行表达式进行合并成新的Project对象。
即使顶部和底部Project操作合并后生成新的Project投影操作newProjects,使用RexUtil.isIdentity判断newProjects与底部Project的输入子RelNode做一次是否相同,如果是强制模式即force=true,直接调用call.transformTo把底部Project投影的子输入RelNode注册RelSet集合内,做一次等价变换然后返回return结束。
inal List<RexNode> newProjects = RelOptUtil.pushPastProject(topProject.getProjects(), bottomProject);//想等于Project进行合并。生成新的Project final RelNode input = bottomProject.getInput(); if (RexUtil.isIdentity(newProjects, input.getRowType())) {//如果新生成的Project和底Project的输入input是否相等。 if (force || input.getRowType().getFieldNames() .equals(topProject.getRowType().getFieldNames())) {//如果是强制模式,字段也相等,直接把底层Project子输入,注册到优化器 call.transformTo(input); return; } } // replace the two projects with a combined projection relBuilder.push(bottomProject.getInput());//使用了底层Project的子input push到构建器,想等于直接移除底层Project relBuilder.project(newProjects, topProject.getRowType().getFieldNames());//使用合并的Project到构建器,做了合并Project变换 call.transformTo(relBuilder.build());
最后,排除两者Project操作相同后,bottomProject.getInput()压入构建器内作为顶层Project操作的子RelNode,使用relBuilder.project方法把newProjects生成新的合并后的Project投影操作,注册到等价的关系表达式集合RelSet。
总结
HiveProjectMergeRule优化规则是把上下两个Project投影操作(RelNode操作符树来讲的上下关系)进行合并,从Sql语句来说,把内外层两个Select进行合并一个Select的优化操作过程,