-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[opt](aggregate) eliminate FD-redundant group-by keys via ANY_VALUE wrapping #64849
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
4c6cfae
4fc3615
b4fc7d8
0daf857
0118902
2e656f9
5574bfa
90e6700
b176280
0501f6a
32cc111
5a90165
d1ecb24
a5d70d0
712a008
0b0d475
ebba1d6
c97bdfc
f84a023
8801cc9
865ea88
8947248
2fa6818
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,90 +17,230 @@ | |
|
|
||
| package org.apache.doris.nereids.rules.rewrite; | ||
|
|
||
| import org.apache.doris.nereids.annotation.DependsRules; | ||
| import org.apache.doris.nereids.jobs.JobContext; | ||
| import org.apache.doris.nereids.properties.DataTrait; | ||
| import org.apache.doris.nereids.properties.FuncDeps; | ||
| import org.apache.doris.nereids.rules.Rule; | ||
| import org.apache.doris.nereids.rules.RuleType; | ||
| import org.apache.doris.nereids.trees.expressions.Alias; | ||
| import org.apache.doris.nereids.trees.expressions.ExprId; | ||
| import org.apache.doris.nereids.trees.expressions.Expression; | ||
| import org.apache.doris.nereids.trees.expressions.NamedExpression; | ||
| import org.apache.doris.nereids.trees.expressions.Slot; | ||
| import org.apache.doris.nereids.trees.expressions.functions.agg.AnyValue; | ||
| import org.apache.doris.nereids.trees.plans.Plan; | ||
| import org.apache.doris.nereids.trees.plans.algebra.Aggregate; | ||
| import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate; | ||
| import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer; | ||
| import org.apache.doris.nereids.trees.plans.logical.LogicalFilter; | ||
| import org.apache.doris.nereids.trees.plans.logical.LogicalProject; | ||
| import org.apache.doris.nereids.trees.plans.visitor.CustomRewriter; | ||
| import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanRewriter; | ||
|
|
||
| import com.google.common.collect.ImmutableList; | ||
| import com.google.common.collect.LinkedHashMultimap; | ||
| import com.google.common.collect.Multimap; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Map.Entry; | ||
| import java.util.Set; | ||
|
|
||
|
|
||
| /** | ||
| * Eliminate group by key based on fd item information. | ||
| * such as: | ||
| * for a -> b, we can get: | ||
| * group by a, b, c => group by a, c | ||
| * | ||
| * When a group-by key is FD-redundant but still needed in the output, | ||
| * it is wrapped with any_value() and assigned a fresh ExprId. | ||
| * Upper plan references are rewritten via ExprIdRewriter so that | ||
| * all ancestor nodes see the new ExprIds. | ||
| */ | ||
| @DependsRules({EliminateGroupBy.class, ColumnPruning.class}) | ||
| public class EliminateGroupByKey implements RewriteRuleFactory { | ||
| public class EliminateGroupByKey extends DefaultPlanRewriter<Map<ExprId, ExprId>> implements CustomRewriter { | ||
| private ExprIdRewriter exprIdReplacer; | ||
|
|
||
| @Override | ||
| public Plan rewriteRoot(Plan plan, JobContext jobContext) { | ||
| if (!plan.containsType(Aggregate.class)) { | ||
| return plan; | ||
| } | ||
| Map<ExprId, ExprId> replaceMap = new HashMap<>(); | ||
| ExprIdRewriter.ReplaceRule replaceRule = new ExprIdRewriter.ReplaceRule(replaceMap, false); | ||
| exprIdReplacer = new ExprIdRewriter(replaceRule, jobContext); | ||
| return plan.accept(this, replaceMap); | ||
| } | ||
|
|
||
| @Override | ||
| public Plan visit(Plan plan, Map<ExprId, ExprId> replaceMap) { | ||
| plan = visitChildren(this, plan, replaceMap); | ||
| plan = exprIdReplacer.rewriteExpr(plan, replaceMap); | ||
|
englefly marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Rewrite lateral This whole-tree replacement can leave The lower deterministic expressions provide valid
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 原有代码的 bug |
||
| return plan; | ||
| } | ||
|
|
||
| @Override | ||
| public Plan visitLogicalProject(LogicalProject<? extends Plan> proj, Map<ExprId, ExprId> replaceMap) { | ||
|
englefly marked this conversation as resolved.
|
||
| proj = visitChildren(this, proj, replaceMap); | ||
|
|
||
| // Find the Aggregate child, possibly through a Filter | ||
| Plan child = proj.child(0); | ||
| LogicalAggregate<? extends Plan> agg; | ||
| boolean hasFilter = child instanceof LogicalFilter; | ||
| if (hasFilter && child.child(0) instanceof LogicalAggregate) { | ||
| agg = (LogicalAggregate<? extends Plan>) child.child(0); | ||
| } else if (child instanceof LogicalAggregate) { | ||
| agg = (LogicalAggregate<? extends Plan>) child; | ||
| } else { | ||
| return exprIdReplacer.rewriteExpr(proj, replaceMap); | ||
| } | ||
|
|
||
| // Don't transform if source repeat is present | ||
| if (agg.getSourceRepeat().isPresent()) { | ||
| return exprIdReplacer.rewriteExpr(proj, replaceMap); | ||
| } | ||
|
|
||
| // Rewrite proj and the filter (if present) through the replaceMap accumulated | ||
| // by visitChildren, so that ExprId replacements from nested rewrites | ||
| // (e.g. inner aggregates) are reflected in the required-output slot set. | ||
| proj = (LogicalProject<? extends Plan>) exprIdReplacer.rewriteExpr(proj, replaceMap); | ||
| if (hasFilter) { | ||
| child = exprIdReplacer.rewriteExpr(child, replaceMap); | ||
| } | ||
|
|
||
| // Compute requireOutput: slots needed by the Project (and Filter, if present) | ||
| Set<Slot> requireOutput = new HashSet<>(proj.getInputSlots()); | ||
|
englefly marked this conversation as resolved.
|
||
| if (hasFilter) { | ||
| requireOutput.addAll(child.getInputSlots()); | ||
| } | ||
|
|
||
| // Transform the aggregate | ||
| EliminateResult result = eliminateGroupByKeyWithMap(agg, requireOutput); | ||
| if (!result.changed) { | ||
| return proj; | ||
| } | ||
|
|
||
| // Merge into the global replaceMap so that all ancestor nodes get rewritten | ||
| replaceMap.putAll(result.replaceMap); | ||
|
|
||
| // Rebuild the child chain with the new aggregate, | ||
| // and rewrite the Filter (if present) and Project expressions | ||
| Plan newChild; | ||
| if (hasFilter) { | ||
| Plan updatedFilter = child.withChildren(result.newAgg); | ||
| newChild = exprIdReplacer.rewriteExpr(updatedFilter, replaceMap); | ||
| } else { | ||
| newChild = result.newAgg; | ||
| } | ||
| Plan newProj = exprIdReplacer.rewriteExpr(proj.withChildren(newChild), replaceMap); | ||
| return newProj; | ||
| } | ||
|
|
||
| @Override | ||
| public List<Rule> buildRules() { | ||
| return ImmutableList.of( | ||
| RuleType.ELIMINATE_GROUP_BY_KEY.build( | ||
| logicalProject(logicalAggregate().when(agg -> !agg.getSourceRepeat().isPresent())) | ||
| .then(proj -> { | ||
| LogicalAggregate<? extends Plan> agg = proj.child(); | ||
| LogicalAggregate<Plan> newAgg = eliminateGroupByKey(agg, proj.getInputSlots()); | ||
| if (newAgg == null) { | ||
| return null; | ||
| } | ||
| return proj.withChildren(newAgg); | ||
| })), | ||
| RuleType.ELIMINATE_FILTER_GROUP_BY_KEY.build( | ||
| logicalProject(logicalFilter(logicalAggregate() | ||
| .when(agg -> !agg.getSourceRepeat().isPresent()))) | ||
| .then(proj -> { | ||
| LogicalAggregate<? extends Plan> agg = proj.child().child(); | ||
| Set<Slot> requireSlots = new HashSet<>(proj.getInputSlots()); | ||
| requireSlots.addAll(proj.child(0).getInputSlots()); | ||
| LogicalAggregate<Plan> newAgg = eliminateGroupByKey(agg, requireSlots); | ||
| if (newAgg == null) { | ||
| return null; | ||
| } | ||
| return proj.withChildren(proj.child().withChildren(newAgg)); | ||
| }) | ||
| ) | ||
| ); | ||
| public Plan visitLogicalCTEConsumer(LogicalCTEConsumer cteConsumer, Map<ExprId, ExprId> replaceMap) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. visitLogicalCTEConsumer this function can be removed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 不能,因为现在这个rule是whole tree rewrite rule. |
||
| // When a producer aggregate's output slot is wrapped with any_value(), | ||
| // a fresh ExprId is recorded in replaceMap. The CTE consumer's producerToConsumerSlotMap | ||
| // still references the old ExprId, so we must rebuild both maps with the new ExprIds. | ||
| Map<Slot, Slot> newConsumerToProducer = new LinkedHashMap<>(); | ||
| Multimap<Slot, Slot> newProducerToConsumer = LinkedHashMultimap.create(); | ||
| for (Slot producerSlot : cteConsumer.getConsumerToProducerOutputMap().values()) { | ||
| ExprId newExprId = resolveExprIdChain(producerSlot.getExprId(), replaceMap); | ||
| Slot effectiveProducerSlot = newExprId != null | ||
| ? (Slot) producerSlot.withExprId(newExprId) | ||
| : producerSlot; | ||
| for (Slot consumerSlot : cteConsumer.getProducerToConsumerOutputMap().get(producerSlot)) { | ||
| newProducerToConsumer.put(effectiveProducerSlot, consumerSlot); | ||
| newConsumerToProducer.put(consumerSlot, effectiveProducerSlot); | ||
| } | ||
| } | ||
| return cteConsumer.withTwoMaps(newConsumerToProducer, newProducerToConsumer); | ||
| } | ||
|
|
||
| /** Follow transitive ExprId chain to find the final replacement, or null if none. */ | ||
| private static ExprId resolveExprIdChain(ExprId exprId, Map<ExprId, ExprId> replaceMap) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. resolveExprIdChain this function can be removed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. visitLogicalCTEConsumer 要使用 |
||
| ExprId newId = replaceMap.get(exprId); | ||
| if (newId == null) { | ||
| return null; | ||
| } | ||
| ExprId lastId = newId; | ||
| while (true) { | ||
| ExprId next = replaceMap.get(lastId); | ||
| if (next == null) { | ||
| return lastId; | ||
| } | ||
| lastId = next; | ||
| } | ||
| } | ||
|
|
||
| /** Result of eliminateGroupByKey: the new aggregate and a map of old->new ExprIds. */ | ||
| private static class EliminateResult { | ||
| final LogicalAggregate<Plan> newAgg; | ||
| final Map<ExprId, ExprId> replaceMap; | ||
| final boolean changed; | ||
|
|
||
| EliminateResult(LogicalAggregate<Plan> newAgg, Map<ExprId, ExprId> replaceMap, boolean changed) { | ||
| this.newAgg = newAgg; | ||
| this.replaceMap = replaceMap; | ||
| this.changed = changed; | ||
| } | ||
| } | ||
|
|
||
| LogicalAggregate<Plan> eliminateGroupByKey(LogicalAggregate<? extends Plan> agg, Set<Slot> requireOutput) { | ||
| Set<Expression> removeExpression = findCanBeRemovedExpressions(agg, requireOutput, | ||
| EliminateResult eliminateGroupByKeyWithMap(LogicalAggregate<? extends Plan> agg, Set<Slot> requireOutput) { | ||
| FindResult result = findCanBeRemovedExpressionsInternal(agg, requireOutput, | ||
| agg.child().getLogicalProperties().getTrait()); | ||
| Set<Expression> removeExpression = result.removeExpression; | ||
| Set<Expression> wrapWithAnyValue = result.wrapWithAnyValue; | ||
|
|
||
| List<Expression> newGroupExpression = new ArrayList<>(); | ||
| for (Expression expression : agg.getGroupByExpressions()) { | ||
| if (!removeExpression.contains(expression)) { | ||
| if (!removeExpression.contains(expression) | ||
| && !wrapWithAnyValue.contains(expression)) { | ||
| newGroupExpression.add(expression); | ||
|
englefly marked this conversation as resolved.
|
||
| } | ||
| } | ||
| List<NamedExpression> newOutput = new ArrayList<>(); | ||
| Map<ExprId, ExprId> replaceMap = new HashMap<>(); | ||
| boolean changed = !removeExpression.isEmpty() || !wrapWithAnyValue.isEmpty(); | ||
| for (NamedExpression expression : agg.getOutputExpressions()) { | ||
| if (!removeExpression.contains(expression)) { | ||
| newOutput.add(expression); | ||
| if (removeExpression.contains(expression)) { | ||
| continue; | ||
| } | ||
| if (wrapWithAnyValue.contains(expression)) { | ||
| // expression is FD-redundant but needed in output: wrap with any_value | ||
| // Use fresh ExprId (auto-generated by Alias) to avoid ExprId collision, | ||
| // and record the mapping for rewriting upper plan references. | ||
| Alias newAlias = new Alias(new AnyValue(expression.toSlot()), expression.getName()); | ||
|
englefly marked this conversation as resolved.
|
||
| replaceMap.put(expression.getExprId(), newAlias.getExprId()); | ||
| expression = newAlias; | ||
| } | ||
| newOutput.add(expression); | ||
| } | ||
| return agg.withGroupByAndOutput(newGroupExpression, newOutput); | ||
| return new EliminateResult(agg.withGroupByAndOutput(newGroupExpression, newOutput), replaceMap, changed); | ||
| } | ||
|
|
||
| /** | ||
| * return removeExpression | ||
| * Return expressions that can be completely removed from both group-by and output. | ||
| * Kept for backward compatibility with external callers (e.g. PushDownAggThroughJoinOnPkFk). | ||
| */ | ||
| public static Set<Expression> findCanBeRemovedExpressions(LogicalAggregate<? extends Plan> agg, | ||
| Set<Slot> requireOutput, DataTrait dataTrait) { | ||
| FindResult result = findCanBeRemovedExpressionsInternal(agg, requireOutput, dataTrait); | ||
| return new HashSet<>(result.removeExpression); | ||
| } | ||
|
|
||
| /** Result of findCanBeRemovedExpressionsInternal: two sets of expressions. */ | ||
| private static class FindResult { | ||
| final Set<Expression> removeExpression; // remove from group-by and output | ||
| final Set<Expression> wrapWithAnyValue; // remove from group-by, wrap with ANY_VALUE in output | ||
|
|
||
| FindResult(Set<Expression> removeExpression, Set<Expression> wrapWithAnyValue) { | ||
| this.removeExpression = removeExpression; | ||
| this.wrapWithAnyValue = wrapWithAnyValue; | ||
| } | ||
| } | ||
|
|
||
| private static FindResult findCanBeRemovedExpressionsInternal(LogicalAggregate<? extends Plan> agg, | ||
| Set<Slot> requireOutput, DataTrait dataTrait) { | ||
| Map<Expression, Set<Slot>> groupBySlots = new HashMap<>(); | ||
| Set<Slot> validSlots = new HashSet<>(); | ||
| for (Expression expression : agg.getGroupByExpressions()) { | ||
|
|
@@ -110,17 +250,24 @@ public static Set<Expression> findCanBeRemovedExpressions(LogicalAggregate<? ext | |
|
|
||
| FuncDeps funcDeps = dataTrait.getAllValidFuncDeps(validSlots); | ||
| if (funcDeps.isEmpty()) { | ||
| return new HashSet<>(); | ||
| return new FindResult(new HashSet<>(), new HashSet<>()); | ||
| } | ||
|
|
||
| Set<Set<Slot>> minGroupBySlots = funcDeps.eliminateDeps(new HashSet<>(groupBySlots.values()), requireOutput); | ||
| Set<Expression> removeExpression = new HashSet<>(); | ||
| Set<Expression> wrapWithAnyValue = new HashSet<>(); | ||
| for (Entry<Expression, Set<Slot>> entry : groupBySlots.entrySet()) { | ||
| if (!minGroupBySlots.contains(entry.getValue()) | ||
| && !requireOutput.containsAll(entry.getValue())) { | ||
| removeExpression.add(entry.getKey()); | ||
| if (!minGroupBySlots.contains(entry.getValue())) { | ||
| // FD redundant: can remove from group-by | ||
| if (!requireOutput.containsAll(entry.getValue())) { | ||
| // Not needed in output either: remove completely | ||
| removeExpression.add(entry.getKey()); | ||
| } else { | ||
| // Still needed in output: remove from group-by, wrap with ANY_VALUE in output | ||
|
englefly marked this conversation as resolved.
|
||
| wrapWithAnyValue.add(entry.getKey()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Suppress invalid scan constraints before this branch This added
Please suppress superclass constraints for raw-version reads and require every constrained column to be present before registering a constraint on a selected index, then add executed result regressions for both modes.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #66801 |
||
| } | ||
| } | ||
| } | ||
| return removeExpression; | ||
| return new FindResult(removeExpression, wrapWithAnyValue); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.