github-actions[bot] commented on code in PR #67940:
URL: https://github.com/apache/doris/pull/67940#discussion_r4063718504
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnCorrelatedApplyAggregateFilter.java:
##########
@@ -97,28 +633,2112 @@ 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());
+ // the predicates of the apply are evaluated on the nodes above the
aggregation of the
+ // subquery, which produce the outputs of that aggregation themselves,
so no output of it has
+ // to be appended to the projections below them
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, null, ImmutableSet.of(),
false));
+ }
+
+ /**
+ * 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, Slot matchMarkerOfTheEmptyDomain,
+ boolean exposesTheMatchMarker, Map<Expression, Expression>
nullableInnerSlots,
+ boolean ignoresTheKeptRowOfAnEmptyDomain) {
+ List<Expression> groupBy = Lists.newArrayList(keys);
+ for (Expression groupByExpression : aggregate.getGroupByExpressions())
{
+ // the group by of an aggregate above the aggregation of the
domain may read the columns
+ // of the inner side as well, and the left outer join of the
domain reports them as
+ // nullable (see nullableInnerSlots)
+ groupBy.add(ExpressionUtils.replace(groupByExpression,
nullableInnerSlots));
+ }
+ if (matchMarkerOfTheEmptyDomain != null && exposesTheMatchMarker) {
+ // The marker of the row which is kept for an empty domain is read
by the guard of the
+ // aggregates above the aggregation of the domain and by the
filters between the
+ // aggregates (see rebuildTheAggregationChain), so those
aggregates expose it. The marker
+ // is null for the row which is kept for an empty domain, so the
grouping of the rows of a
+ // correlation key does not change. The top aggregate does not
expose it: the rows which it
+ // produces are the rows of the subquery, and the marker belongs
to the rows below it (the
+ // aggregates above the aggregation of the domain read it from
their own input).
+ groupBy.add(matchMarkerOfTheEmptyDomain);
+ }
+ List<NamedExpression> outputs = Lists.newArrayList();
+ if (!ignoresTheKeptRowOfAnEmptyDomain) {
+ // The row which the rewrite keeps for an empty correlated domain
is the row which the
+ // original subquery computes out of the empty input of the
aggregation of the domain (its
+ // own guard makes that aggregation return the value of an empty
input, see
+ // guardAggregateArguments), so the aggregates above it read the
value which that row
+ // carries (the count 0 of
+ // select (select count(*) from t2 where t2.c1 = t1.c1 having
count(*) = 0) from t1, for
+ // example). They read the columns of the inner side of the join
through the nullable slots.
+ for (NamedExpression output : aggregate.getOutputExpressions()) {
+ outputs.add((NamedExpression) ExpressionUtils.replace(output,
nullableInnerSlots));
+ }
+ } else {
+ // The subquery produces no row for an empty correlated domain (a
grouped aggregation of
+ // the domain reports an empty domain as "no row", and a HAVING
clause which does not hold
+ // for the row of a global aggregation of the domain removes that
row), so the aggregates
+ // above the aggregation of the domain ignore the row which the
rewrite keeps for that
+ // domain: they return the value of their empty input for it (see
guardAggregateArguments),
+ // which is the value the original subquery computes for the empty
domain as well.
+ Set<AggregateFunction> aggregates = Sets.newLinkedHashSet();
+ for (NamedExpression output : aggregate.getOutputExpressions()) {
+
aggregates.addAll(output.collect(AggregateFunction.class::isInstance));
+ }
+ Map<Expression, Expression> compensated =
guardAggregateArguments(aggregates,
+ matchMarkerOfTheEmptyDomain);
+ if (compensated == null) {
+ // an aggregate of the aggregation cannot be guarded, so the
row which is kept for an
+ // empty input cannot be told apart from a row of the rows
below the aggregation
+ return null;
+ }
+ for (NamedExpression output : aggregate.getOutputExpressions()) {
+ outputs.add((NamedExpression) ExpressionUtils.replace(
+ (NamedExpression) ExpressionUtils.replace(output,
compensated), nullableInnerSlots));
+ }
+ }
+ keys.forEach(key -> outputs.add((NamedExpression) key));
Review Comment:
[P1] Expose the empty-domain marker from every intermediate aggregate. For a
three-stage chain such as `o.k NOT IN (SELECT max(m) FROM (SELECT max(c) m FROM
(SELECT count(*) c FROM i WHERE i.k=o.k GROUP BY i.g) x) y)`, the keep-empty
path rewrites the middle MAX with `exposesTheMatchMarker=true`: it groups by
the marker and guards `MAX(c)` with it, but this line outputs only the original
expressions plus the correlation keys. `LogicalAggregate.computeOutput()`
therefore omits the marker, while the rewritten top MAX reads it in
`MAX(IF(marker,m,NULL))` (and an intervening Project may also be asked to carry
it), so `CheckAfterRewrite` rejects the valid query. Append the marker to the
explicit outputs whenever it must be exposed, and cover a 3+ aggregate chain.
Please also model HAVING filters at every aggregate boundary: the current
helper only relaxes the interval immediately above the domain aggregate, so
fixing this missing slot alone exposes the higher-HAVING NULL-versus-empty e
rror.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnCorrelatedApplyAggregateFilter.java:
##########
@@ -97,28 +633,2112 @@ 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());
+ // the predicates of the apply are evaluated on the nodes above the
aggregation of the
+ // subquery, which produce the outputs of that aggregation themselves,
so no output of it has
+ // to be appended to the projections below them
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, null, ImmutableSet.of(),
false));
+ }
+
+ /**
+ * 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, Slot matchMarkerOfTheEmptyDomain,
+ boolean exposesTheMatchMarker, Map<Expression, Expression>
nullableInnerSlots,
+ boolean ignoresTheKeptRowOfAnEmptyDomain) {
+ List<Expression> groupBy = Lists.newArrayList(keys);
+ for (Expression groupByExpression : aggregate.getGroupByExpressions())
{
+ // the group by of an aggregate above the aggregation of the
domain may read the columns
+ // of the inner side as well, and the left outer join of the
domain reports them as
+ // nullable (see nullableInnerSlots)
+ groupBy.add(ExpressionUtils.replace(groupByExpression,
nullableInnerSlots));
+ }
+ if (matchMarkerOfTheEmptyDomain != null && exposesTheMatchMarker) {
+ // The marker of the row which is kept for an empty domain is read
by the guard of the
+ // aggregates above the aggregation of the domain and by the
filters between the
+ // aggregates (see rebuildTheAggregationChain), so those
aggregates expose it. The marker
+ // is null for the row which is kept for an empty domain, so the
grouping of the rows of a
+ // correlation key does not change. The top aggregate does not
expose it: the rows which it
+ // produces are the rows of the subquery, and the marker belongs
to the rows below it (the
+ // aggregates above the aggregation of the domain read it from
their own input).
+ groupBy.add(matchMarkerOfTheEmptyDomain);
+ }
+ List<NamedExpression> outputs = Lists.newArrayList();
+ if (!ignoresTheKeptRowOfAnEmptyDomain) {
+ // The row which the rewrite keeps for an empty correlated domain
is the row which the
+ // original subquery computes out of the empty input of the
aggregation of the domain (its
+ // own guard makes that aggregation return the value of an empty
input, see
+ // guardAggregateArguments), so the aggregates above it read the
value which that row
+ // carries (the count 0 of
+ // select (select count(*) from t2 where t2.c1 = t1.c1 having
count(*) = 0) from t1, for
+ // example). They read the columns of the inner side of the join
through the nullable slots.
+ for (NamedExpression output : aggregate.getOutputExpressions()) {
+ outputs.add((NamedExpression) ExpressionUtils.replace(output,
nullableInnerSlots));
+ }
+ } else {
+ // The subquery produces no row for an empty correlated domain (a
grouped aggregation of
+ // the domain reports an empty domain as "no row", and a HAVING
clause which does not hold
+ // for the row of a global aggregation of the domain removes that
row), so the aggregates
+ // above the aggregation of the domain ignore the row which the
rewrite keeps for that
+ // domain: they return the value of their empty input for it (see
guardAggregateArguments),
+ // which is the value the original subquery computes for the empty
domain as well.
+ Set<AggregateFunction> aggregates = Sets.newLinkedHashSet();
+ for (NamedExpression output : aggregate.getOutputExpressions()) {
+
aggregates.addAll(output.collect(AggregateFunction.class::isInstance));
+ }
+ Map<Expression, Expression> compensated =
guardAggregateArguments(aggregates,
+ matchMarkerOfTheEmptyDomain);
+ if (compensated == null) {
+ // an aggregate of the aggregation cannot be guarded, so the
row which is kept for an
+ // empty input cannot be told apart from a row of the rows
below the aggregation
+ return null;
+ }
+ for (NamedExpression output : aggregate.getOutputExpressions()) {
+ outputs.add((NamedExpression) ExpressionUtils.replace(
+ (NamedExpression) ExpressionUtils.replace(output,
compensated), nullableInnerSlots));
+ }
+ }
+ keys.forEach(key -> outputs.add((NamedExpression) key));
+ return new LogicalAggregate<>(groupBy, outputs, aggregate.child(0));
+ }
+
+ /**
+ * Whether the rewrite of the outer side has to keep one row for the
correlation keys whose rows
+ * below the aggregation of the domain are missing, although that
aggregation returns no row of its
+ * own for them: every aggregate above it is global, so the aggregation of
the original subquery
+ * produces one row for the empty input of such a key, and the aggregates
which the rewrite builds
+ * above that aggregation can be guarded with the marker of the row which
is kept for it (see
+ * guardAggregateArguments and withTheKeysInTheGroupBy). The value which
that row exposes is then
+ * the value which the original subquery exposes for the key. For example
the subquery of
+ *
+ * select o.k from o where o.k in (
+ * select coalesce(max(c), 0) from
+ * (select count(*) as c from i where i.k = o.k group by i.g)
x)
+ *
+ * returns one row whose value is 0 for the outer rows whose correlated
domain is empty (the max of
+ * the empty derived table is null and the coalesce turns that null into
the 0), so the outer row
+ * of the value 0 matches the subquery: the rewrite keeps a row for such a
key, the max above it
+ * ignores that row and returns the null of its empty input, and the
coalesce of the plan of the
+ * subquery turns that null into the 0 as well.
+ *
+ * An EXISTS subquery reads whether the row of such a key exists instead
of the value it exposes,
+ * so the row has to be kept when the HAVING clause of the subquery keeps
the row of the empty
+ * input (see the EXISTS branch below).
+ */
+ private static boolean keepsTheRowOfAnEmptyDomain(LogicalApply<?, ?>
apply, TheAggregation aggregation,
+ CorrelatedAggregatePredicates predicates) {
+ List<LogicalAggregate<?>> chain = aggregation.aggregationChain();
+ List<LogicalAggregate<?>> aboveTheDomain = chain.subList(0,
chain.size() - 1);
+ if (aboveTheDomain.isEmpty()) {
+ // the aggregation of the domain is the only aggregation of the
subquery: the rewrite of the
+ // outer side keeps a row of its own for an empty domain when that
aggregation is global,
+ // and no aggregate above it observes such a row
+ return false;
+ }
+ if (aboveTheDomain.stream().anyMatch(aggregate ->
!aggregate.getGroupByExpressions().isEmpty())) {
+ // an aggregate above the aggregation of the domain groups the
rows which it reads, so the
+ // row which is kept for an empty domain builds a group of its own
in that aggregate, while
+ // the aggregation of the original subquery produces no row at all
for such an empty input
+ return false;
+ }
+ if (!chain.stream()
+ .flatMap(aggregate ->
aggregate.getOutputExpressions().stream())
+ .flatMap(output ->
output.collect(AggregateFunction.class::isInstance).stream())
+ .allMatch(function -> function instanceof
NullIgnoringAggregateFunction)) {
+ // only the aggregates which ignore null arguments can be guarded,
so that the row which is
+ // kept for an empty domain does not contribute to them (see
guardAggregateArguments)
+ return false;
+ }
+ if (apply.isExist()) {
+ // The row which the aggregation of an empty correlated domain
produces exists for the
+ // subquery when the HAVING clause of the aggregation above the
one of the domain holds for
+ // the values of that empty input (see
havingMayHoldWithEmptyInput): the EXISTS of 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
(the max of the empty
+ // derived table is null and the HAVING clause keeps that row).
The rewrite keeps the row of
+ // such a key and lets the aggregates above the aggregation of the
domain return the values
+ // of an empty input for it, so that the nodes above the
aggregation decide on the row the
+ // way the original subquery does (see rebuildTheAggregationChain
and
+ // guardAggregateArguments). A HAVING clause which rejects the row
of the empty input
+ // (having max(c) > 0, for example) drops it, and the nodes above
the aggregation reject
+ // the row which the rewrite keeps for such a key as well.
+ List<Expression> havingConjuncts = predicates.havingPredicates();
+ return aboveTheDomain.stream()
+ .filter(aggregate ->
aggregate.getGroupByExpressions().isEmpty())
+ .anyMatch(aggregate ->
havingMayHoldWithEmptyInput(aggregate,
+ Sets.newLinkedHashSet(havingConjuncts)));
+ }
+ return true;
+ }
+
+ /**
+ * 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 a
rewrite which dropped the
+ * key would produce no row for it and the semi join would drop the outer
row. The aggregation of
+ * the inner side is not equivalent for such subqueries, and the
aggregation of the outer side is
+ * only equivalent when it keeps a row for the empty domain and lets the
aggregates above the
+ * aggregation of the domain return the values of an empty input for it
(see
+ * keepsTheRowOfAnEmptyDomain); the caller reports the subqueries which
neither of them can
+ * rewrite.
+ */
+ private static boolean
observesTheEmptyInputOfAGlobalAggregate(LogicalApply<?, ?> apply,
+ TheAggregation aggregation, CorrelatedAggregatePredicates
predicates) {
+ if (keepsTheRowOfAnEmptyDomain(apply, aggregation, predicates)) {
+ // the rewrite of the outer side keeps the row which such a key is
missing (see
+ // keepsTheRowOfAnEmptyDomain), so the subquery is not reported
+ return false;
+ }
+ return theEmptyInputOfAGlobalAggregateIsObservable(apply, aggregation,
predicates);
+ }
+
+ /**
+ * The detection of observesTheEmptyInputOfAGlobalAggregate on its own:
the subqueries
+ * which this detection reports are the subqueries whose rewrite would
drop the row which the
+ * aggregation of the original subquery returns for a correlation key
whose rows below the
+ * aggregation of the domain are missing. The rewrite of the outer side
keeps that row and lets the
+ * aggregates above the aggregation of the domain return the values of an
empty input for it when
+ * every one of them is a global aggregate which ignores null arguments
(see
+ * keepsTheRowOfAnEmptyDomain), and those subqueries are rewritten instead
of reported.
+ */
+ private static boolean
theEmptyInputOfAGlobalAggregateIsObservable(LogicalApply<?, ?> apply,
+ TheAggregation aggregation, CorrelatedAggregatePredicates
predicates) {
+ List<LogicalAggregate<?>> chain = aggregation.aggregationChain();
+ if (chain.get(chain.size() - 1).getGroupByExpressions().isEmpty()
+ &&
theFiltersBetweenTheAggregationOfTheDomainAndTheOneAboveIt(aggregation).isEmpty())
{
+ // the aggregation of the domain returns a row for every
correlation key, so no
+ // aggregate above it can observe an empty input (a filter between
those aggregations is
+ // the HAVING clause of the aggregation of the domain: it decides
on the row of the empty
+ // input and may reject it, which the aggregates above it observe)
+ 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).
+ if (aboveTheDomain.stream()
+
.anyMatch(UnCorrelatedApplyAggregateFilter::returnsAValueForAnEmptyInput)) {
+ return true;
+ }
+ // The nodes above the aggregation of the domain may expose a value of
their own for the empty
+ // input as well, even though the aggregates are nullable: the
projection of the subquery of
+ //
+ // select o.k from o where o.k in (
+ // select coalesce(max(c), 0) from
+ // (select count(*) as c from i where i.k = o.k group by
i.g) x)
+ //
+ // turns the null which the max of the empty derived table returns
into the 0 which an outer
+ // row with the value 0 compares with, while the rewrite has no row to
compare it with and the
+ // semi join drops that row (see exposesAValueForAnEmptyInput).
+ if (exposesAValueForAnEmptyInput(apply, aggregation, aboveTheDomain)) {
+ return true;
+ }
+ // The missing row of a key is observable when the result of the IN is
not read as the decision
+ // on the outer row alone: the null which the subquery of the original
query compares with
+ // (the row of the aggregation of an empty derived table, for example)
makes the IN unknown,
+ // while the rewrite compares with nothing, which is false for an IN
and true for a NOT IN.
+ // The result of an IN which is used as a value is its mark, so its
null and its false are
+ // observable as well (the plan of such an IN is a mark join). For
example the subquery of
+ //
+ // select o.k from o where o.k not in (
+ // select max(c) from (select count(*) as c from i where i.k =
o.k group by i.g) x)
+ //
+ // returns one row for the outer rows whose correlated domain is empty
(the max of the empty
+ // derived table is null), so their NOT IN is unknown and those rows
are not returned, while
+ // the rewrite produces no row for those keys and their NOT IN is
true. A global aggregate
+ // above the aggregation of the domain is what makes such a row
disappear: the key is added to
+ // the group by of that aggregate (see withTheKeysInTheGroupBy), so a
key without rows below it
+ // has no group at all.
+ return (apply.isNot() || apply.getMarkJoinSlotReference().isPresent())
+ && aboveTheDomain.stream()
+ .anyMatch(aggregate ->
aggregate.getGroupByExpressions().isEmpty());
+ }
+
+ /**
+ * Whether one of the aggregates of the aggregation declares a value of
its own for an empty
+ * input (the count 0 or the empty array of an array_agg, for example). An
aggregate which
+ * declares no such value is left to the rewrite of the other cases, which
reads the value of an
+ * empty input the way the rest of the engine does (see
keepsTheValueOfAnEmptyDomain): the value
+ * of a UDAF is written in the UDAF itself, so its declaration does not
tell it.
+ */
+ private static boolean returnsAValueForAnEmptyInput(LogicalAggregate<?>
aggregate) {
+ for (NamedExpression output : aggregate.getOutputExpressions()) {
+ if (output.collect(AggregateFunction.class::isInstance).stream()
+ .anyMatch(function -> function instanceof
NotNullableAggregateFunction)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * The filters which sit between the aggregation of the domain and the
aggregation above it: they
+ * decide on the rows which the aggregation of the domain produces (they
are the HAVING clauses of
+ * the aggregation below them), so they are not evaluated for a
correlation key whose rows below
+ * that aggregation are missing (see keepsTheRowOfAnEmptyDomain).
+ */
+ private static Set<LogicalFilter>
theFiltersBetweenTheAggregationOfTheDomainAndTheOneAboveIt(
+ TheAggregation aggregation) {
+ if (aggregation.topAggregation() == aggregation.domainAggregation()) {
+ // the aggregation of the domain is the only aggregation of the
subquery, so there is no
+ // aggregation above it and no filter between such aggregations
either (the filter of the
+ // domain itself reads the rows of the domain and is not a filter
between the aggregates)
+ return ImmutableSet.of();
+ }
+ List<LogicalAggregate<?>> chain = aggregation.aggregationChain();
+ Plan below = chain.get(chain.size() - 2).child(0);
+ Set<LogicalFilter> filters = Sets.newLinkedHashSet();
+ while (below != aggregation.domainAggregation()) {
+ if (below instanceof LogicalFilter) {
+ filters.add((LogicalFilter) below);
+ }
+ below = below.child(0);
+ }
+ return filters;
+ }
+
+ /**
+ * Whether the nodes above the top aggregate expose a value of their own
for the empty input of a
+ * correlation key: the input of such a key is empty when the aggregation
of the domain has no row
+ * for it and a global aggregate above that aggregation returns no row for
that key in the
+ * rewrite, while the aggregation of the original subquery computes the
nodes above it out of the
+ * row of the empty input. The projection of the subquery of
+ *
+ * select o.k from o where o.k in (
+ * select coalesce(max(c), 0) from
+ * (select count(*) as c from i where i.k = o.k group by i.g)
x)
+ *
+ * turns the null which the max of the empty derived table returns into
the 0 which the outer row
+ * with the value 0 compares with, while the rewrite has no row to compare
it with: the semi join
+ * would drop that outer row. The value which a node computes for the
empty input is read by
+ * replacing the outputs of the top aggregate with the values they return
for it and folding the
+ * expression.
+ */
+ private static boolean exposesAValueForAnEmptyInput(LogicalApply<?, ?>
apply, TheAggregation aggregation,
+ List<LogicalAggregate<?>> aboveTheDomain) {
+ if (aboveTheDomain.stream().noneMatch(aggregate ->
aggregate.getGroupByExpressions().isEmpty())) {
+ // every aggregate above the aggregation of the domain groups the
rows it reads, so a key
+ // without rows below that aggregation has no group in those
aggregates either
+ return false;
+ }
+ Map<Expression, Expression> emptyValues = Maps.newHashMap();
+ Set<Slot> outputsOfTheTopAggregation = Sets.newHashSet();
+ for (NamedExpression output :
aggregation.topAggregation().getOutputExpressions()) {
+ Expression expression = output instanceof Alias ? ((Alias)
output).child() : output;
+ if (!(expression instanceof AggregateFunction)) {
+ continue;
+ }
+ Expression emptyValue =
emptyValueForEmptyInput((AggregateFunction) expression);
+ if (emptyValue == null) {
+ // the aggregate declares no value for an empty input: it
returns the null of the empty
+ // input (the max of no row, for example)
+ emptyValue = new NullLiteral(output.getDataType());
+ }
+ emptyValues.put(output.toSlot(), emptyValue);
+ outputsOfTheTopAggregation.add(output.toSlot());
+ }
+ if (emptyValues.isEmpty()) {
+ return false;
+ }
+ Plan below = apply.right();
+ while (below != aggregation.topAggregation()) {
+ if (below instanceof LogicalProject) {
+ for (NamedExpression project : ((LogicalProject<?>)
below).getProjects()) {
+ Expression expression = project instanceof Alias ?
((Alias) project).child() : project;
+ if (Sets.intersection(expression.getInputSlots(),
outputsOfTheTopAggregation).isEmpty()) {
+ // the projection does not read the aggregation of the
domain
+ continue;
+ }
+ Expression folded =
FoldConstantRuleOnFE.evaluateWithoutContext(
+ ExpressionUtils.replace(expression, emptyValues));
+ if (folded instanceof Literal && !(folded instanceof
NullLiteral)) {
+ // the nodes above the aggregation expose this value
for the empty input
+ return true;
+ }
+ }
+ }
+ below = below.child(0);
+ }
+ return false;
+ }
+
+ /**
+ * Replace every aggregate of the chain with its rewritten version and
expose the keys through
+ * the projections between them, so that every aggregate above the deepest
one can group by the
+ * keys. The walk stops at the deepest aggregate: its rewritten version
already reads the rows of
+ * the filter below it (see pullUpCorrelatedFilter).
+ *
+ * The projections below the top aggregate only expose the keys, while the
projections above it
+ * expose the outputs of the top aggregate as well: the aggregate which
defines those outputs
+ * sits above the projections below it, so they cannot produce them. The
outputs to expose are
+ * therefore dropped as soon as the walk reaches the top aggregate.
+ *
+ * For example the plan of the example of TheAggregation is rewritten into
+ *
+ * Apply(correlationFilter=[t2.c1 = t1.c1])
+ * |-- t1
+ * +-- Filter(sum(x.c) > 2)
+ * +-- Aggregate(group by [key.c1], output [sum(c) as
sum(x.c), key.c1])
+ * +-- Project([c, key.c1]) [the key
exposed between the
+ * +-- Filter(c > 1)
aggregates]
+ * +-- Aggregate(group by [key.c1, t2.c2],
+ * output [t2.c2, count(*) as c,
key.c1])
+ * +-- t2
+ */
+ private static Plan rebuildTheAggregationChain(Plan plan, TheAggregation
aggregation,
+ Map<LogicalAggregate<?>, Plan> newAggregations, Set<Slot>
keysToExpose,
+ Set<Slot> outputsOfTheTopAggregation, Slot
matchMarkerOfTheEmptyDomain,
+ Set<LogicalFilter> filtersWhichKeepTheRowOfAnEmptyDomain, boolean
belowTheTopAggregate) {
+ Plan replacement = newAggregations.get(plan);
+ if (plan == aggregation.domainAggregation()) {
+ return replacement;
+ }
+ // the nodes below the top aggregate cannot produce its outputs, so
they only carry the keys
+ Plan child = rebuildTheAggregationChain(plan.child(0), aggregation,
newAggregations, keysToExpose,
+ plan == aggregation.topAggregation() ? ImmutableSet.of() :
outputsOfTheTopAggregation,
+ matchMarkerOfTheEmptyDomain,
filtersWhichKeepTheRowOfAnEmptyDomain,
+ belowTheTopAggregate || plan == aggregation.topAggregation());
+ if (replacement != null) {
+ return replacement.withChildren(child);
+ }
+ if (filtersWhichKeepTheRowOfAnEmptyDomain.contains(plan)) {
+ // The row which the rewrite keeps for an empty domain is not a
row of the rows below this
+ // filter (its marker is null), and the aggregation of the
original subquery does not
+ // evaluate the filter for the empty input of such a key either:
the row passes the filter,
+ // so that the aggregates above it return the values of an empty
input for the key (see
+ // keepsTheRowOfAnEmptyDomain). Without the relaxation the filter
would remove the row which
+ // the rewrite keeps for the key, and the aggregates above it
(which the rewrite grouped by
+ // the correlation key) would produce no row at all for the key.
+ List<Expression> conjuncts = Lists.newArrayList();
+ for (Expression conjunct : ((LogicalFilter<Plan>)
plan).getConjuncts()) {
+ conjuncts.add(ExpressionUtils.or(conjunct, new
IsNull(matchMarkerOfTheEmptyDomain)));
Review Comment:
[P2] Rewrite retained wrapper expressions through `nullableInnerSlots` too.
With a declared-NOT-NULL `i.g`, `o.k NOT IN (SELECT max(c) FROM (SELECT
count(*) c FROM i WHERE i.k=o.k GROUP BY i.g HAVING i.g + count(*) > 0) x)`
retains `Project(c) -> Filter(i.g + c > 0) -> grouped Aggregate`. The
keep-empty LEFT OUTER join makes `i.g` nullable and the aggregate now uses that
nullable slot, but this Filter is rebuilt from its original nonnullable `i.g`
(Projects below follow the same pattern). Final `AdjustNullable` finds the same
ExprId with nullable=true and throws when `fe_debug` is enabled. Pass the
nullable replacement map through chain reconstruction and apply it to retained
Filter/Project expressions; add a `fe_debug` planner/result case for a NOT NULL
group key.
--
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]