github-actions[bot] commented on code in PR #67940:
URL: https://github.com/apache/doris/pull/67940#discussion_r4052475691
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SubExprAnalyzer.java:
##########
@@ -125,7 +125,10 @@ public Expression visitInSubquery(InSubquery expr, T
context) {
AnalyzedResult analyzedResult = analyzeSubquery(expr);
checkOutputColumn(analyzedResult.getLogicalPlan());
- checkNoCorrelatedSlotsUnderAgg(analyzedResult);
+ // the correlated predicate of an IN subquery may sit below the
aggregation of the subquery:
+ // the rewrite which unnests it (UnCorrelatedApplyAggregateFilter)
computes the aggregation
+ // of the domain of every outer row, the empty correlated domain
included, so that the value
+ // which the IN compares exists for every outer row
Review Comment:
[P1] Keep unsupported outer references under IN aggregates behind a narrowed
analyzer guard. This removal also admits `o.k IN (SELECT sum(i.v + o.k) FROM
i)`: unlike the scalar path, `visitInSubquery` never runs
`CorrelatedSlotsValidator`, so the outer slot in the aggregate is accepted. The
resulting plan is `Apply(IN) -> [Scan(o), Aggregate[sum(i.v + o.k)] ->
Scan(i)]` with correlation slots but no correlation filter.
`UnCorrelatedApplyAggregateFilter` requires a correlated filter below the
aggregate and never matches, while `InApplyToJoin` emits only the IN equality.
The right aggregate still reads `o.k` from a scan that cannot produce it, so
`CheckAfterRewrite` rejects the newly admitted query. This is distinct from the
existing retained-project issue, which starts with a supported domain filter
and loses a rewrite-added key. Either restrict this removal to the filter shape
the PR handles, or extend unnesting to aggregate arguments/grouping/join
predicates and add a negative or
result oracle.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnCorrelatedApplyAggregateFilter.java:
##########
@@ -97,28 +559,1746 @@ public List<Rule> buildRules() {
return apply;
}
- // pull up correlated filter into apply node
- List<NamedExpression> newAggOutput = new
ArrayList<>(agg.getOutputExpressions());
- List<Expression> newGroupby =
- Utils.getUnCorrelatedExprs(correlatedPredicate,
apply.getCorrelationSlot());
- newGroupby.addAll(agg.getGroupByExpressions());
+ CorrelatedAggregatePredicates predicates =
+ CorrelatedAggregatePredicates.of(apply, correlatedPredicate,
+ aggregation.filtersAboveTheAggregation());
+ // A global aggregate above an aggregate which can return no row for a
correlation key
+ // returns a row for the empty input of that key, and neither rewrite
can reproduce it (see
+ // observesTheEmptyInputOfAGlobalAggregate): report those subqueries
instead of dropping the
+ // row and evaluating the subquery to false.
+ if (observesTheEmptyInputOfAGlobalAggregate(apply, aggregation,
predicates)) {
+ throw new AnalysisException("Unsupported correlated subquery with
grouping and/or aggregation "
+ + apply.right());
+ }
+ if (needCorrelatedAggregationOnOuter(apply, aggregation,
correlatedPredicate, predicates)) {
+ Plan aggregatedOuter = pullUpCorrelatedPredicateByAggregatingOuter(
+ apply, aggregation, unCorrelatedPredicate, predicates);
+ if (aggregatedOuter != null) {
+ return aggregatedOuter;
+ }
+ // The original rewrite is known to be not equivalent for this
subquery and the rewrite
+ // above cannot be applied safely: report the subquery as
unsupported instead of building
+ // a plan whose result is wrong.
+ throw new AnalysisException("Unsupported correlated subquery with
grouping and/or aggregation "
+ + apply.right());
+ }
+
+ // pull up correlated filter into apply node: the inner side of every
correlated predicate
+ // becomes a group by column and an output column of the aggregation
below the filter, so that
+ // the aggregation of one outer row is the aggregation of the rows of
its own key, and every
+ // aggregate above that aggregation groups the rows of its child by
the same keys (a scalar
+ // subquery keeps the rows of its aggregation through an aggregation
which SubqueryToApply adds
+ // above it, and those rows may not be mixed between two correlation
keys either)
+ List<Expression> newGroupby =
Utils.getUnCorrelatedExprs(correlatedPredicate, apply.getCorrelationSlot());
Map<Expression, Slot> unCorrelatedExprToSlot = Maps.newHashMap();
+ List<NamedExpression> newGroupbyOutputs =
Lists.newArrayListWithCapacity(newGroupby.size());
for (Expression expression : newGroupby) {
if (expression instanceof Slot) {
- newAggOutput.add((NamedExpression) expression);
+ newGroupbyOutputs.add((NamedExpression) expression);
} else {
Alias alias = new Alias(expression);
unCorrelatedExprToSlot.put(expression, alias.toSlot());
- newAggOutput.add(alias);
+ newGroupbyOutputs.add(alias);
}
}
+ // the keys which the aggregates above the deepest one group by: the
slots the keys have in
+ // the output of the aggregation below them
+ List<NamedExpression> keySlots = newGroupbyOutputs.stream()
+
.map(NamedExpression::toSlot).collect(ImmutableList.toImmutableList());
correlatedPredicate = ExpressionUtils.replace(correlatedPredicate,
unCorrelatedExprToSlot);
- LogicalAggregate newAgg = new LogicalAggregate<>(newGroupby,
newAggOutput,
-
PlanUtils.filterOrSelf(ImmutableSet.copyOf(unCorrelatedPredicate),
filter.child()));
+ Map<LogicalAggregate<?>, Plan> newAggregations = new
IdentityHashMap<>();
+ for (LogicalAggregate<?> aggregate : aggregation.aggregationChain()) {
+ boolean isTheAggregationOfTheDomain = aggregate ==
aggregation.domainAggregation();
+ List<Expression> groupBy = Lists.newArrayList(
+ isTheAggregationOfTheDomain ? newGroupby : keySlots);
+ groupBy.addAll(aggregate.getGroupByExpressions());
+ List<NamedExpression> outputs =
Lists.newArrayList(aggregate.getOutputExpressions());
+ outputs.addAll(isTheAggregationOfTheDomain ? newGroupbyOutputs :
keySlots);
+ Plan child = isTheAggregationOfTheDomain
+ // the projections below it only carry the columns which
the aggregation needs, so
+ // the new aggregation reads the rows of the filter
directly
+ ?
PlanUtils.filterOrSelf(ImmutableSet.copyOf(unCorrelatedPredicate),
+ aggregation.domainFilter().child())
+ : aggregate.child(0);
+ newAggregations.put(aggregate, new LogicalAggregate<>(groupBy,
outputs, child));
+ }
+ // the predicates which were already pulled into the apply are the
predicates of the HAVING
+ // clause of the subquery: they were evaluated on the rows of the old
aggregate and have to
+ // stay in the filter of the new apply, otherwise the subquery loses
them
+ List<Expression> newCorrelationFilter = Lists.newArrayList();
+ apply.getCorrelationFilter().map(ExpressionUtils::extractConjunction)
+ .ifPresent(newCorrelationFilter::addAll);
+ newCorrelationFilter.addAll(correlatedPredicate);
+ // the join which unnests the apply reads the inner side of the
correlation predicates from
+ // the output of the right side, so the projections which wrap the new
aggregate have to
+ // expose the keys it added: an IN subquery keeps the projections of
its select list above
+ // the aggregate (for example the outputs [c1] and [c1, c2] which wrap
an aggregate
+ // computing count(*) as c1, random() as c2), and a projection which
hides one of the keys
+ // makes the apply unresolvable
+ Set<Slot> keysToExpose = keySlots.stream()
+
.map(NamedExpression::toSlot).collect(ImmutableSet.toImmutableSet());
+ // The outputs of the top aggregate are exposed by the projections
above that aggregate
+ // alone, because the projections below it cannot produce them: the
aggregate which defines
+ // them sits above those projections. The predicates which were pulled
into the apply read
+ // the outputs of the top aggregate as well (for example the max(c) <=
t1.c1 of the HAVING
+ // clause), and the projection below the top aggregate has to carry
the keys alone. For
+ // example the subquery of
+ //
+ // select t1.c1 from t1 where t1.c1 in (select max(c) from (select
count(*) as c from t2
+ // where t2.c1 = t1.c1 group by t2.c2) x having max(c) <=
t1.c1)
+ //
+ // reaches the rewrite with the plan
+ //
+ // Apply(correlationFilter=[(max(c) <= t1.c1)])
+ // |-- t1
+ // +-- Project([max(c)]) [the
select list]
+ // +-- Aggregate(group by [], output [max(c) as max(c)])
+ // +-- Project([c]) [the
projection below the
+ // +-- Aggregate(group by [t2.c2],
aggregate which defines
+ // output [t2.c2, count(*) as c]) max(c)]
+ // +-- Filter(t2.c1 = t1.c1)
+ // +-- t2
+ //
+ // and appending max(c) to the projection of the count (the projection
below the aggregate
+ // which defines it) would make that projection read a slot which its
child cannot produce,
+ // so the plan would be rejected by the slot check of the rewrite.
+ Set<Slot> outputsOfTheTopAggregation = newCorrelationFilter.stream()
+ .flatMap(conjunct -> conjunct.getInputSlots().stream())
+ .filter(slot ->
newAggregations.get(aggregation.topAggregation()).getOutput().contains(slot))
+ .filter(slot -> !keysToExpose.contains(slot))
+ .collect(ImmutableSet.toImmutableSet());
return new LogicalApply<>(apply.getCorrelationSlot(),
apply.getSubqueryType(), apply.isNot(),
apply.getCompareExpr(), apply.getTypeCoercionExpr(),
- ExpressionUtils.optionalAnd(correlatedPredicate),
apply.getMarkJoinSlotReference(),
+ ExpressionUtils.optionalAnd(newCorrelationFilter),
apply.getMarkJoinSlotReference(),
apply.isNeedAddSubOutputToProjects(),
apply.isMarkJoinSlotNotNull(), apply.left(),
- isRightChildAgg ? newAgg : apply.right().withChildren(newAgg));
+ rebuildTheAggregationChain(apply.right(), aggregation,
newAggregations, keysToExpose,
+ outputsOfTheTopAggregation));
+ }
+
+ /**
+ * The aggregation of the subquery with the keys of the correlation added
to its group by and to
+ * its output: the rows of one correlation key are the rows of the
subquery for the outer rows
+ * which own that key, so an aggregation above the aggregation of the
domain may not mix them.
+ * For example the aggregate of the sum of the example of TheAggregation
is rewritten into
+ *
+ * Aggregate(group by [key.c1], output [sum(c) as sum(x.c), key.c1])
+ *
+ * around the rewritten aggregation of the domain, whose rows carry the
key as well (see
+ * rebuildTheAggregationChain).
+ */
+ private static LogicalAggregate<?>
withTheKeysInTheGroupBy(LogicalAggregate<?> aggregate,
+ List<? extends Expression> keys) {
+ List<Expression> groupBy = Lists.newArrayList(keys);
+ groupBy.addAll(aggregate.getGroupByExpressions());
+ List<NamedExpression> outputs =
Lists.newArrayList(aggregate.getOutputExpressions());
+ keys.forEach(key -> outputs.add((NamedExpression) key));
+ return new LogicalAggregate<>(groupBy, outputs, aggregate.child(0));
+ }
+
+ /**
+ * Whether the aggregation of the subquery holds a global aggregate above
an aggregate which can
+ * return no row for a correlation key, and the subquery observes the row
which that global
+ * aggregate returns for the empty input.
+ *
+ * The rewrite adds the correlation keys to the group by of every
aggregate of the chain (see
+ * pullUpCorrelatedFilter and withTheKeysInTheGroupBy), so a global
aggregate above the
+ * aggregation of the domain produces no row at all for a key whose rows
below it are missing,
+ * while the aggregation of the original subquery returns one row for that
empty input. The
+ * subquery of
+ *
+ * select t1.c1 from t1 where exists (select max(c) from (select
count(*) as c from t2
+ * where t2.c1 = t1.c1 group by t2.c2) x having max(c) is null)
+ *
+ * is true for the outer rows whose correlated domain is empty, because
the max of the empty
+ * derived table is null and the HAVING clause keeps that row, while the
rewrite produces no row
+ * for those keys and the semi join drops the outer row. Neither the
aggregation of the inner
+ * side (a global aggregate above the aggregation of the domain would
aggregate the rows of every
+ * correlation key together) nor the aggregation of the outer side (it
groups that aggregate by
+ * the correlation key) is equivalent for such subqueries, so the caller
reports them.
+ */
+ private static boolean
observesTheEmptyInputOfAGlobalAggregate(LogicalApply<?, ?> apply,
+ TheAggregation aggregation, CorrelatedAggregatePredicates
predicates) {
+ List<LogicalAggregate<?>> chain = aggregation.aggregationChain();
+ if (chain.get(chain.size() - 1).getGroupByExpressions().isEmpty()) {
+ // the aggregation of the domain returns a row for every
correlation key, so no
+ // aggregate above it can observe an empty input
+ return false;
+ }
+ // the aggregates above the deepest one: the deepest one reads the
rows of the domain of a
+ // correlation key, and the predicates of that domain may leave them
empty
+ List<LogicalAggregate<?>> aboveTheDomain = chain.subList(0,
chain.size() - 1);
+ if (apply.isExist()) {
+ // the row which the global aggregate returns for the empty input
decides whether the
+ // EXISTS reports the outer row, unless the nodes above the
aggregation reject that row
+ // (the predicates of the HAVING clause which reference the outer
query were pulled into
+ // the apply, and they decide on the row of the empty input as
well)
+ List<Expression> havingConjuncts = predicates.havingPredicates();
+ return aboveTheDomain.stream()
+ .filter(aggregate ->
aggregate.getGroupByExpressions().isEmpty())
+ .anyMatch(aggregate ->
havingMayHoldWithEmptyInput(aggregate,
+ Sets.newLinkedHashSet(havingConjuncts)));
+ }
+ if (apply.isScalar()) {
+ // A scalar subquery exposes the output of the aggregation of its
domain: the join of
+ // the rewrite reports a null for the keys whose rows below the
aggregation are missing,
+ // and SubqueryToApply repairs that null with the nvl of the value
which the top
+ // aggregate returns for an empty input. A global aggregate below
the top aggregate
+ // changes the value which the top one computes out of the row of
the empty input.
+ boolean hasAGlobalAggregateBelowTheTop =
aboveTheDomain.stream().skip(1)
+ .anyMatch(aggregate ->
aggregate.getGroupByExpressions().isEmpty());
+ return hasAGlobalAggregateBelowTheTop
+ && (returnsAValueForAnEmptyInput(chain.get(0)) ||
aboveTheDomain.stream().skip(1)
+
.anyMatch(UnCorrelatedApplyAggregateFilter::returnsAValueForAnEmptyInput));
+ }
+ // An IN subquery compares the outer value with the value of the
aggregation of its domain:
+ // the value which a global aggregate returns for an empty input can
match the outer value,
+ // while the rewrite has no row to compare it with (a null value of
the aggregation does not
+ // match either, so an aggregation of nullable aggregates alone is
left alone).
Review Comment:
[P1] Preserve the NULL row for nested nullable aggregates used by IN/NOT IN.
For `o.k NOT IN (SELECT max(c) FROM (SELECT count(*) c FROM i WHERE i.k=o.k
GROUP BY i.g) x)`, an empty correlated domain makes the original global `MAX`
return one NULL row, so NOT IN is UNKNOWN and the outer row is rejected.
Because `MAX` is not a `NotNullableAggregateFunction`, this branch permits the
rewrite; adding the key to both aggregates then leaves no lower row carrying
that key, so the upper aggregate returns no row and `NOT IN (empty)` becomes
TRUE. A mark/value IN similarly changes from NULL to false. This differs from
the existing nested-EXISTS/HAVING report: this new detector handles that branch
but treats nullable IN results as unobservable, which is only true for positive
IN used directly as a filter. Reject or preserve nullable global stages for NOT
IN and mark/value contexts, and add those oracles.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnCorrelatedApplyAggregateFilter.java:
##########
@@ -17,75 +17,537 @@
package org.apache.doris.nereids.rules.rewrite;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.hint.DistributeHint;
import org.apache.doris.nereids.rules.Rule;
import org.apache.doris.nereids.rules.RuleType;
+import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
+import org.apache.doris.nereids.trees.copier.DeepCopierContext;
+import org.apache.doris.nereids.trees.copier.LogicalPlanDeepCopier;
import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.ComparisonPredicate;
+import org.apache.doris.nereids.trees.expressions.EqualPredicate;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+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.Not;
+import org.apache.doris.nereids.trees.expressions.NullSafeEqual;
import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.VolatileExpression;
+import org.apache.doris.nereids.trees.expressions.functions.AlwaysNotNullable;
+import
org.apache.doris.nereids.trees.expressions.functions.NoneMovableFunction;
+import org.apache.doris.nereids.trees.expressions.functions.Udf;
+import
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Avg;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Max;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Min;
+import
org.apache.doris.nereids.trees.expressions.functions.agg.NotNullableAggregateFunction;
+import
org.apache.doris.nereids.trees.expressions.functions.agg.NullIgnoringAggregateFunction;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.If;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
+import org.apache.doris.nereids.trees.plans.DistributeType;
+import org.apache.doris.nereids.trees.plans.JoinType;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
import org.apache.doris.nereids.trees.plans.logical.LogicalApply;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalSort;
+import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
import org.apache.doris.nereids.util.ExpressionUtils;
import org.apache.doris.nereids.util.PlanUtils;
import org.apache.doris.nereids.util.Utils;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
/**
- * Merge the correlated predicate and agg in the filter under apply.
- * And keep the unCorrelated predicate under agg.
- * <p>
- * Use the correlated column as the group by column of agg,
- * the output column is the correlated column and the input column.
- * <pre>
- * before:
- * apply
- * / \
- * Input(output:b) Filter(this node's existence depends on having
clause's existence)
- * |
- * agg(output:fn; group by:null)
- * |
- * Filter(correlated predicate(Input.e = this.f)/Unapply
predicate)
- *
- * end:
- * apply(correlated predicate(Input.e = this.f))
- * / \
- * Input(output:b) Filter(this node's existence depends on having clause's
existence)
- * |
- * agg(output:fn,this.f; group by:this.f)
- * |
- * Filter(Uncorrelated predicate)
- * </pre>
+ * Pull the correlated predicates of a subquery which aggregates out of the
subquery, so that the
+ * aggregation computes the aggregation of the correlated domain of one outer
row instead of the
+ * aggregation of all the inner rows of the subquery.
+ *
+ * The correlated predicates sit in the filter below the aggregation of the
subquery before the
+ * rewrite (its WHERE clause): the right side of the apply is the nodes above
that filter (the
+ * HAVING clause, the projections of the select list, the aggregation), the
filter itself and the
+ * inner table. This rule pulls the correlated conjuncts of that filter into
the apply (they become
+ * the conditions which unnest the apply) and rebuilds the aggregation around
them, keeping the
+ * nodes above it in place; the uncorrelated conjuncts stay in the filter
below the aggregation.
+ *
+ * The shape of the subquery selects one of two strategies for the new
aggregation:
+ *
+ * The aggregation stays on the inner side when the inner rows of one outer
row are exactly the
+ * groups of the aggregation: the inner side of the correlated predicate is
added to the group by
+ * of the aggregation and to its output (so that the conditions of the apply
can read the key), and
+ * every outer row is paired with the group of its own domain. This needs an
equality between the
+ * outer side and the inner side (eg. t2.c1 = t1.c1), because the value of the
inner side is what
+ * selects the group of the outer row. For the subquery of
+ *
+ * select t1.c1 from t1 where exists (select count(*) from t2
+ * where t2.c1 = t1.c1 group by t2.c2 having count(*) > 0)
+ *
+ * the rewritten plan is (the EXISTS reads the aggregation through a semi
join, see below):
+ *
+ * LEFT SEMI JOIN (t2.c1 = t1.c1) [the correlation
filter of the apply]
+ * |-- t1
+ * +-- Filter(count(*) > 0) [the HAVING
clause, kept in place]
+ * +-- Aggregate(group by [t2.c1, t2.c2], output [t2.c1, t2.c2,
count(*)])
+ * +-- t2
+ *
+ * The aggregation is built on the outer side in every other case: a deep copy
of the outer plan
+ * computes the distinct correlation keys, the keys are joined with the inner
side on the
+ * predicates of the domain, the result is grouped by the keys, and every
outer row is paired with
+ * the aggregation of its own key (see
pullUpCorrelatedPredicateByAggregatingOuter). For the
+ * subquery of
+ *
+ * select t1.c1 from t1 where exists (select count(*) from t2
+ * where t2.c1 < t1.c1 group by t2.c2 having count(*) = 2)
+ *
+ * the rewritten plan is:
+ *
+ * LEFT SEMI JOIN (t1.c1 <=> key.c1)
+ * |-- t1
+ * +-- Filter(count(*) = 2)
+ * +-- Aggregate(group by [key.c1, t2.c2], output [key.c1, t2.c2,
count(*)])
+ * +-- INNER JOIN (t2.c1 < key.c1)
+ * |-- Aggregate(group by [t1.c1], output [t1.c1])
[the keys of t1]
+ * | +-- t1
+ * +-- t2
+ *
+ * The two properties which select the strategy are independent: whether the
correlated predicate
+ * is an equality or not, and whether the aggregation has a group by or not (a
global aggregate).
+ * The four combinations:
+ *
+ * 1. equality + group by: inner side (the first example). The domain of an
outer row is exactly
+ * the group whose key is the value of the inner side, so the HAVING clause
of that group is the
+ * HAVING clause of the outer row, and an outer row with an empty domain
has no group at all,
+ * exactly like the grouped aggregate of the subquery.
+ *
+ * 2. non-equality + group by: outer side (the second example). The domain of
an outer row is the
+ * union of several groups, so the aggregates of the subquery (eg.
count(*)) are computed over
+ * the union, while one group of the inner side holds a part of the domain
only.
+ *
+ * 3. non-equality, no group by: outer side, for the reason of 2. For the
subquery of
+ *
+ * select t1.c1 from t1 where exists (select count(*) from t2
+ * where t2.c1 < t1.c1 having count(*) = 0)
+ *
+ * the plan is the plan of 2. without the group by of the subquery, with
the row of the empty
+ * domain kept by a left outer join (a global aggregate returns a row for
an empty correlated
+ * domain, and the count of that row has to be 0):
+ *
+ * LEFT SEMI JOIN (t1.c1 <=> key.c1)
+ * |-- t1
+ * +-- Filter(count($correlation_match_marker) = 0)
+ * +-- Aggregate(group by [key.c1], output [key.c1,
count($correlation_match_marker)])
+ * +-- LEFT OUTER JOIN (t2.c1 < key.c1)
[keeps the empty domain]
+ * |-- Aggregate(group by [t1.c1], output [t1.c1])
+ * | +-- t1
+ * +-- Project([true AS $correlation_match_marker,
t2.c1])
+ * +-- t2
+ *
+ * 4. equality, no group by: inner side when the subquery does not need the
row which the global
+ * aggregate returns for an empty correlated domain, outer side when it
does, because that row
+ * has no group on the inner side to be produced from. For the subquery of
+ *
+ * select t1.c1 from t1 where exists (select count(*) from t2
+ * where t2.c1 = t1.c1 having count(*) > 0)
+ *
+ * the HAVING clause is false for the empty input, so the row of the empty
domain disappears
+ * from the result anyway and the aggregation stays on the inner side (the
plan of 1. without
+ * the group by of the subquery); for the subquery of
+ *
+ * select t1.c1 from t1 where exists (select count(*) from t2
+ * where t2.c1 = t1.c1 having count(*) = 0)
+ *
+ * the row of the empty domain survives (its count is 0), so the
aggregation is built on the
+ * outer side (the plan of 3. with the equality of the domain):
+ *
+ * LEFT SEMI JOIN (t1.c1 <=> key.c1)
+ * |-- t1
+ * +-- Filter(count($correlation_match_marker) = 0)
+ * +-- Aggregate(group by [key.c1], output [key.c1,
count($correlation_match_marker)])
+ * +-- LEFT OUTER JOIN (t2.c1 = key.c1)
[keeps the empty domain]
+ * |-- Aggregate(group by [t1.c1], output [t1.c1])
+ * | +-- t1
+ * +-- Project([true AS $correlation_match_marker,
t2.c1])
+ * +-- t2
+ *
+ * The three subquery types differ in what they need from the aggregation (see
+ * needCorrelatedAggregationOnOuter for the exact conditions):
+ *
+ * - EXISTS/NOT EXISTS is decided by the aggregation, which this rule reads
through a LEFT SEMI
+ * JOIN (a LEFT ANTI JOIN for NOT EXISTS): it needs the row of an empty
correlated domain only
+ * when that row can decide the subquery (a HAVING clause which may hold for
it, or a node which
+ * the subquery keeps above its aggregation), see the examples of 4.
+ * - IN/NOT IN compares the outer expression with the value of the subquery,
which is the first
+ * column of the aggregation (the keys are appended after the outputs of the
subquery, so this
+ * column does not move). The row which a global aggregate returns for an
empty correlated domain
+ * is part of the values the IN compares: the subquery of
+ *
+ * select t1.c1 from t1 where t1.c1 in (select count(*) from t2 where
t2.c1 = t1.c1)
+ *
+ * compares the outer row with the count 0 of its empty domain (k in (0)
holds for the outer row
+ * 0), so its aggregation is built on the outer side (the plan of 3. with
the count of the
+ * marker) and the rewrite keeps the apply, whose correlation filter pairs
every outer row with
+ * the aggregation of its own key; the rule which converts the IN into a
join then compares the
+ * outer expression with the first column of that aggregation. That row is
needed for every
+ * aggregate, whatever value it returns for an empty input: the aggregation
of the inner side has
+ * no group for an outer row without a match, and the comparison which the
rewrite of the IN builds
+ * reads the rows of the domain of one outer row, so the null which the left
outer join keeps for it
+ * is not the value of its empty domain for the IN. The not in of
+ *
+ * select t1.c1 from t1 where t1.c1 not in (select sum(t2.c2) from t2
where t2.c1 = t1.c1)
+ *
+ * returns true for the outer rows without a match instead of the null which
the sum of the empty
+ * domain produces. A grouped aggregate has no value for an empty domain, so
the subquery of
+ *
+ * select t1.c1 from t1 where t1.c1 in (select count(*) from t2
+ * where t2.c1 = t1.c1 group by t2.c2)
+ *
+ * keeps the aggregation on the inner side (the plan of 1. with the
projection of the select list
+ * kept below the apply, see PullUpProjectUnderApply):
+ *
+ * Apply(IN, correlationFilter=[(t2.c1 = t1.c1)])
+ * |-- t1
+ * +-- Project([count(*), t2.c1]) [the select list,
the key appended]
+ * +-- Aggregate(group by [t2.c1, t2.c2], output [t2.c2,
count(*), t2.c1])
+ * +-- t2
+ * - scalar exposes the value of the aggregation as the value of the subquery.
The left outer join
+ * which pairs an outer row with the aggregation of its own key returns null
for the outer rows
+ * whose domain is empty, which is the value of those outer rows for the
aggregates whose value
+ * for an empty input the rewrite reads that way (see
keepsTheValueOfAnEmptyDomain): the subquery of
+ *
+ * select t1.c1, (select max(t2.c2) from t2 where t2.c1 = t1.c1) from t1
+ *
+ * keeps the aggregation on the inner side (max returns null for an empty
input, which is the
+ * null of the join), while the subquery of
+ *
+ * select t1.c1, (select count(*) from t2 where t2.c1 < t1.c1) from t1
+ *
+ * is built on the outer side (the correlated predicate is not an equality,
and the count of an
+ * empty domain is 0, which no null of the inner side can produce):
+ *
+ * LEFT OUTER JOIN (t1.c1 <=> key.c1)
+ * |-- t1
+ * +-- Aggregate(group by [key.c1], output [key.c1,
count($correlation_match_marker)])
+ * +-- LEFT OUTER JOIN (t2.c1 < key.c1)
[keeps the empty domain]
+ * |-- Aggregate(group by [t1.c1], output [t1.c1])
+ * | +-- t1
+ * +-- Project([true AS $correlation_match_marker, t2.c1])
+ * +-- t2
+ *
+ * To summarize, the aggregation of the subquery stays on the inner side for
the subqueries whose
+ * correlated domain of one outer row is one group of the aggregation and
whose empty domain needs
+ * no row: every equality predicate with a group by, the global aggregates
whose HAVING clause
+ * rejects the empty input (or whose value for the empty input is the null
which the left outer
+ * join keeps), the grouped IN subqueries, and the scalar subqueries of the
aggregates whose empty
+ * value the rewrite reads from that null. It is built on the outer side for
the subqueries whose
+ * domain is several groups of the aggregation (a non-equality predicate,
whatever the subquery type
+ * and the aggregate are), and for the subqueries which need the row of an
empty correlated domain (a
+ * global aggregate whose HAVING clause may hold for the empty input, an IN
subquery which compares
+ * the value of a global aggregate, an EXISTS subquery which keeps nodes above
its aggregation, a
+ * scalar subquery whose empty value is not the null of the join).
+ *
+ * Keeping the aggregation on the inner side in the cases of the outer side
returns a wrong result,
+ * which is why the rewrite of the outer side has to exist. The subquery of
+ *
+ * select cq_o.k from cq_o where exists (select count(*) from cq_i
+ * where cq_i.k = cq_o.k having count(*) = 0)
+ *
+ * returns the outer row 7 when its domain is empty (the count of an empty
domain is 0, which the
+ * HAVING clause accepts), while the aggregation of the inner side groups the
inner rows by
+ * cq_i.k: the outer row 7 has no group there, so its row would be dropped.
The subquery of
+ *
+ * select cn_o.k from cn_o where exists (select count(*) from cn_i
+ * where cn_i.k < cn_o.k group by cn_i.g having count(*) = 2)
+ *
+ * returns the outer row 3 when its domain is the two rows (1, 10) and (2, 10)
of the single group
+ * g = 10 (the count of the domain is 2), while the aggregation of the inner
side groups the inner
+ * rows by (cn_i.k, cn_i.g): those two rows are two groups of count 1 and the
HAVING clause would
+ * never hold.
*/
public class UnCorrelatedApplyAggregateFilter implements RewriteRuleFactory {
+
+ /**
+ * name of the projected column which tells whether an inner row matched
the correlated
+ * predicate: the left outer join of the aggregation of the outer side
keeps one row whose
+ * marker is null for every correlation key which has no inner row, so the
aggregations of this
+ * column see the empty correlated domain as an empty input (see
guardAggregateArguments). For
+ * example the plan of the second case of needCorrelatedAggregationOnOuter
evaluates
+ * count($correlation_match_marker) over the rows of Project([true AS
+ * $correlation_match_marker, t2.c1]), and the row which the left outer
join keeps for an empty
+ * domain has a null marker, which that count does not count.
+ */
+ private static final String CORRELATION_MATCH_MARKER =
"$correlation_match_marker";
+
@Override
public List<Rule> buildRules() {
+ // The nodes between the apply and the aggregate of the subquery are
projections and filters
+ // in any order, so the rules cannot enumerate their shapes: match
every correlated apply
+ // whose right side starts with one of them and locate the aggregate
in the rule.
return ImmutableList.of(
- logicalApply(any(), logicalAggregate(logicalFilter()))
- .when(LogicalApply::isCorrelated)
-
.then(UnCorrelatedApplyAggregateFilter::pullUpCorrelatedFilter)
- .toRule(RuleType.UN_CORRELATED_APPLY_AGGREGATE_FILTER),
- logicalApply(any(),
logicalFilter(logicalAggregate(logicalFilter())))
+ logicalApply(any(), subTree(LogicalAggregate.class,
LogicalProject.class, LogicalFilter.class))
.when(LogicalApply::isCorrelated)
+ .when(apply -> locateAggregate(apply).isPresent())
.then(UnCorrelatedApplyAggregateFilter::pullUpCorrelatedFilter)
-
.toRule(RuleType.UN_CORRELATED_APPLY_FILTER_AGGREGATE_FILTER));
+
.toRule(RuleType.UN_CORRELATED_APPLY_AGGREGATE_FILTER));
+ }
+
+ /**
+ * The aggregation of the subquery: the aggregates between the apply and
the filter which holds
+ * the predicates of its WHERE clause (from the one below the apply to the
one above that
+ * filter), the filters whose predicates decide which rows of that
aggregation survive (the
+ * HAVING clause of the subquery), and the filter of the WHERE clause
itself.
+ *
+ * The nodes of the subquery of
+ *
+ * select t1.c1 from t1 where exists (
+ * select sum(x.c) from (select count(*) as c from t2
+ * where t2.c1 = t1.c1 group by t2.c2 having count(*) > 1) x
having sum(x.c) > 2)
+ *
+ * before the rewrite, with the member which holds every node:
+ *
+ * Apply(correlationFilter=empty)
[the apply of the subquery]
+ * |-- t1
+ * +-- Filter(sum(x.c) > 2) [havingFilter,
filtersAboveTheAggregation(0)]
+ * +-- Aggregate(group by [], output [sum(c) as sum(x.c)])
[chain(0) =
+ * +-- Project([c])
topAggregation]
+ * +-- Filter(c > 1)
[filtersAboveTheAggregation(1)]
+ * +-- Aggregate(group by [t2.c2], output
[t2.c2, count(*) as c])
+ * ...
[chain(1) =
+ * +-- Project([t2.c2])
domainAggregation]
+ * +-- Filter(t2.c1 = t1.c1)
[domainFilter]
+ * +-- t2
+ *
+ * - chain: every aggregate between the apply and the filter of the WHERE
clause, from the top
+ * down (here the sum over the derived table, then the count of that
table);
+ * - topAggregation: chain(0), the aggregate below the apply, which the
whole chain is rebuilt
+ * around;
+ * - domainAggregation: the deepest aggregate of the chain, which reads
the rows of the filter
+ * of the WHERE clause;
+ * - filtersAboveTheAggregation: the filters whose predicates decide on
the rows of the
+ * aggregation below them (here both HAVING clauses: the sum(x.c) > 2 of
the subquery and the
+ * count(*) > 1 of the derived table); a filter whose predicate selects
the rows of the
+ * domain of an outer row instead is not one of them, see
selectsTheRowsOfTheDomain;
+ * - havingFilter: the deepest filter above the topAggregation, i.e. the
first filter met when
+ * walking down from the apply (here the filter of sum(x.c) > 2, which
sits directly above
+ * the aggregation of the sum);
+ * - domainFilter: the filter which holds the predicates of the WHERE
clause (here
+ * t2.c1 = t1.c1, which the walk finds when it stops below the deepest
aggregate).
+ *
+ * A subquery may wrap its aggregation with further aggregations:
SubqueryToApply adds a
+ * count(*)/any_value(*) aggregation above the aggregation of a correlated
scalar subquery whose
+ * output is used in the outer scope and which has no top level scalar
aggregation (the count is
+ * the runtime check of the scalar subquery, the any_value is its value),
and the user may
+ * aggregate the aggregation of a derived table (the example above). The
rows of the subquery
+ * for one outer row are the rows of the whole chain for the correlation
key of that row, so
+ * every aggregate of the chain has to keep the keys in its group by (see
pullUpCorrelatedFilter
+ * and withTheKeysInTheGroupBy).
+ */
+ private static final class TheAggregation {
+ /** the aggregates between the apply and the filter of the WHERE
clause, from the top down */
+ private final List<LogicalAggregate<?>> chain;
+ /** every filter above the deepest aggregate, whose predicates decide
on the aggregation rows */
+ private final List<LogicalFilter<Plan>> filtersAboveTheAggregation;
+ /** the deepest filter above the top aggregate, which sits directly
below the projections above it */
+ private final Optional<LogicalFilter<Plan>> havingFilter;
+ /** the filter which holds the predicates of the WHERE clause of the
subquery */
+ private final LogicalFilter<Plan> domainFilter;
+
+ private TheAggregation(List<LogicalAggregate<?>> chain,
+ List<LogicalFilter<Plan>> filtersAboveTheAggregation,
+ Optional<LogicalFilter<Plan>> havingFilter,
LogicalFilter<Plan> domainFilter) {
+ this.chain = chain;
+ this.filtersAboveTheAggregation = filtersAboveTheAggregation;
+ this.havingFilter = havingFilter;
+ this.domainFilter = domainFilter;
+ }
+
+ /** the aggregate which reads the rows of the filter, at the bottom of
the chain */
+ private LogicalAggregate<?> domainAggregation() {
+ return chain.get(chain.size() - 1);
+ }
+
+ /** the aggregate below the apply, which the whole chain is rebuilt
around */
+ private LogicalAggregate<?> topAggregation() {
+ return chain.get(0);
+ }
+
+ /** the aggregates between the apply and the filter, from the top down
*/
+ private List<LogicalAggregate<?>> aggregationChain() {
+ return chain;
+ }
+
+ /** every filter above the deepest aggregate, whose predicates decide
on the aggregation rows */
+ private List<LogicalFilter<Plan>> filtersAboveTheAggregation() {
+ return filtersAboveTheAggregation;
+ }
+
+ /** the filter which holds the predicates of the WHERE clause of the
subquery */
+ private LogicalFilter<Plan> domainFilter() {
+ return domainFilter;
+ }
+
+ /** whether the subquery does not wrap its aggregation with another
aggregation */
+ private boolean onlyTheAggregationOfTheDomain() {
+ return chain.size() == 1;
+ }
+ }
+
+ /**
+ * Locate the aggregation of the subquery, walking down from the right
side of the apply: the
+ * nodes above its aggregates are the projections and the filters of the
subquery, the
+ * projections between its aggregates only carry the columns which they
need, and the filter
+ * below the deepest aggregate holds the predicates of its WHERE clause.
The walk stops at a
+ * filter whose child is not an aggregate: that filter is the filter of
the WHERE clause, while
+ * a filter which sits directly above an aggregate (through projections)
is a HAVING clause of
+ * that aggregate.
+ *
+ * The steps of the walk for the subquery of
+ *
+ * select t1.c1 from t1 where exists (
+ * select sum(x.c) from (select count(*) as c from t2
+ * where t2.c1 = t1.c1 group by t2.c2 having count(*) > 1) x
having sum(x.c) > 2)
+ *
+ * whose plan is (see TheAggregation):
+ *
+ * Apply(t1, Filter(sum(x.c) > 2) - Aggregate(sum) - Project([c]) -
Filter(c > 1)
+ * - Aggregate(count) - Project([t2.c2]) - Filter(t2.c1 = t1.c1) -
t2)
+ *
+ * 1. the first loop walks down from the apply while the node is not an
aggregate: the filter
+ * sum(x.c) > 2 is remembered as the havingFilter and added to
filtersAboveTheAggregation,
+ * and the walk stops at the aggregate of the sum (the topAggregation
of the chain);
+ * 2. the second loop walks down from that aggregate: the projection of c
carries a column of
+ * the node below it alone, so it is walked through; the filter c > 1
sits above the
+ * aggregate of the count (through that projection) and its predicate
reads the output of
+ * that aggregate, so it decides on its rows (a HAVING clause of that
aggregate): it is
+ * added to filtersAboveTheAggregation and the chain continues below it;
+ * 3. the aggregate of the count is the deepest aggregate of the chain
(the domainAggregation,
+ * which reads the rows of the filter of the WHERE clause);
+ * 4. the projection of t2.c2 carries a column of the node below it alone,
so it is walked
+ * through, and the walk stops at the filter t2.c1 = t1.c1: its child
is a scan and not an
+ * aggregate, so it is the domainFilter, which holds the predicates of
the WHERE clause (the
+ * correlated predicate is one of them). The walk returns an empty
result (the rule leaves
+ * the apply alone) when a node of the subquery is neither a projection
nor a filter nor an
+ * aggregate, when a projection below an aggregate computes one of its
columns instead of
+ * passing a column of the node below it through, and when the filter
of the WHERE clause
+ * has no child.
+ */
+ private static Optional<TheAggregation> locateAggregate(LogicalApply<?, ?>
apply) {
+ Plan below = apply.right();
+ Optional<LogicalFilter<Plan>> havingFilter = Optional.empty();
+ List<LogicalFilter<Plan>> filtersAboveTheAggregation =
Lists.newArrayList();
+ while (!(below instanceof LogicalAggregate)) {
+ if (below instanceof LogicalFilter) {
+ havingFilter = Optional.of((LogicalFilter<Plan>) below);
+ filtersAboveTheAggregation.add((LogicalFilter<Plan>) below);
+ } else if (!(below instanceof LogicalProject)) {
+ return Optional.empty();
+ }
+ below = below.child(0);
+ }
+ List<LogicalAggregate<?>> chain = Lists.newArrayList();
+ chain.add((LogicalAggregate<?>) below);
+ Plan belowAggregate = below.child(0);
+ LogicalFilter<Plan> domainFilter = null;
+ while (true) {
+ if (belowAggregate instanceof LogicalProject) {
+ for (NamedExpression project : ((LogicalProject<?>)
belowAggregate).getProjects()) {
+ if (!(project instanceof Slot)) {
Review Comment:
[P1] Handle computed scalar outputs between the cardinality wrapper and
grouped aggregate. A newly admitted query such as `SELECT o.k, (SELECT count(*)
+ 1 FROM i WHERE i.k=o.k GROUP BY i.g) FROM o` normalizes to `Apply(SCALAR) ->
wrapper Aggregate[count(*), any_value(v)] -> Project[count + 1 AS v] -> grouped
Aggregate[count] -> Filter[i.k=o.k]`. This check rejects the computed project,
and the fallback matcher only handles `Aggregate -> Project -> Filter`, so no
rule pulls up `i.k=o.k`; `ScalarApplyToJoin` then takes the no-filter path and
`CheckAfterRewrite` rejects the retained outer slot. This is distinct from the
existing project-key reports: those rewrites run and then hide/reference a
newly added key, while this supported WHERE correlation never reaches the
rewrite. Rebuild or carry this normalized project chain (or reject it at the
widened analyzer boundary), and cover computed grouped-scalar outputs with
zero, one, and multiple groups.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]