HyukjinKwon commented on code in PR #59402:
URL: https://github.com/apache/spark/pull/59402#discussion_r4285028512
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteDistinctAggregates.scala:
##########
@@ -223,27 +230,47 @@ object RewriteDistinctAggregates extends
Rule[LogicalPlan] {
case a: Aggregate if mayNeedtoRewrite(a) => rewrite(a)
}
- def rewrite(a: Aggregate): Aggregate = {
+ /**
+ * Groups distinct aggregate expressions by their unfoldable children.
Aggregates that
+ * share a key can share one Expand projection (or the single-distinct
physical path).
+ */
+ private def groupDistinctAggs(
+ aggExpressions: Seq[AggregateExpression]): Map[ExpressionSet,
Seq[AggregateExpression]] = {
+ aggExpressions.filter(_.isDistinct).groupBy { e =>
+ val unfoldableChildren =
ExpressionSet(e.aggregateFunction.children.filter(!_.foldable))
+ if (unfoldableChildren.nonEmpty) {
+ // Only expand the unfoldable children
+ unfoldableChildren
+ } else {
+ // If aggregateFunction's children are all foldable
+ // we must expand at least one of the children (here we take the first
child),
+ // or If we don't, we will get the wrong result, for example:
+ // count(distinct 1) will be explained to count(1) after the rewrite
function.
+ // Generally, the distinct aggregateFunction should not run
+ // foldable TypeCheck for the first child.
+ ExpressionSet(e.aggregateFunction.children.take(1))
+ }
+ }
+ }
+
+ private def numDistinctAggGroups(a: Aggregate): Int =
+ groupDistinctAggs(collectAggregateExprs(a)).size
+ def rewrite(origAgg: Aggregate): Aggregate = {
+ // Keep the normalized form only when it strictly reduces the number of
distinct
+ // groups. Otherwise shapes like COUNT(DISTINCT IF) + SUM(DISTINCT IF)
(same IF)
+ // or a repeated COUNT(DISTINCT IF) next to COUNT(*) would move from the
+ // single-distinct path onto an Expand with no fan-out win.
+ val normalized = normalizeCountDistinctConditional(origAgg)
+ val a =
+ if (numDistinctAggGroups(normalized) < numDistinctAggGroups(origAgg)) {
Review Comment:
**Non-blocking (P2):** `normalizeCountDistinctConditional` rewrites each
eligible COUNT on its own, and this gate only compares total group counts. So a
COUNT whose `IF` is also the distinct child of an aggregate that stays
un-normalized is still split off, as long as other conditional counts make the
total drop. For example:
```sql
SELECT k,
COUNT(DISTINCT IF(c1, x, NULL)), SUM(DISTINCT IF(c1, x, NULL)),
COUNT(DISTINCT IF(c2, x, NULL)), COUNT(DISTINCT IF(c3, x, NULL))
FROM t GROUP BY k
```
With the config off there are three groups, and COUNT and SUM share `{IF(c1,
x, NULL)}`, so the first aggregate holds d1 + d2 + d3 + 3 keys per `k` (where
di = distinct(x | ci)). With it on, the three COUNTs collapse into a guarded
`{x}` group and the SUM keeps `{IF(c1, x, NULL)}`. That is 2 < 3 groups, so the
gate keeps the rewrite, but c1's keys are now carried twice: distinct(x | c1 OR
c2 OR c3) + 1 in the guarded group plus d1 + 1 in the SUM's group. With a broad
c1 over a near-unique `x`, that roughly doubles the partial-aggregate output
and shuffle, while Expand only goes from 3 to 2 projections. Results are the
same, but this shape no longer meets the 'never worse than the uncollapsed
plan' bound in the PR description.
Could `normalizeCountDistinctConditional` skip a COUNT when its `IF`/`CASE`
child (canonicalized) is also the distinct child of an aggregate it won't
normalize? Then the shared group stays intact, and only counts whose original
groups actually disappear get collapsed.
**Verification:**
- **Regression:** For the four-aggregate shared-IF shape, the optimized plan
has one distinct group whose key is the shared IF(c1, x, NULL) serving both
COUNT and SUM, and a guarded {x} group whose Or covers exactly c2 and c3.
- **Behavior:** That query returns the same results with the config on and
off.
- **Compatibility:** Existing coverage for the guard shape, the
strict-reduction gate shapes, subquery exclusions, and raw-key fallbacks still
passes.
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/RewriteDistinctAggregatesSuite.scala:
##########
@@ -125,4 +126,594 @@ class RewriteDistinctAggregatesSuite extends PlanTest {
fail(s"Plan is not as expected:\n$rewrite")
}
}
+
+ //
---------------------------------------------------------------------------
+ // COUNT(DISTINCT IF/CASE) canonicalization (SPARK-56898)
+ //
---------------------------------------------------------------------------
+
+ val conditionalTestRelation = LocalRelation(
+ Symbol("a").int, Symbol("b").int, Symbol("c").int, Symbol("d").string)
+
+ private def countDistinctIf(cond: Expression, base: Expression): Expression
= {
+ Count(If(cond, base, Literal(null))).toAggregateExpression(isDistinct =
true)
+ }
+
+ private def countDistinctCaseWhen(cond: Expression, base: Expression):
Expression = {
+ val caseWhen = CaseWhen(
+ Seq((cond, base)),
+ None)
+ Count(caseWhen).toAggregateExpression(isDistinct = true)
+ }
+
+ private def countDistinctCaseWhenElseNull(cond: Expression, base:
Expression): Expression = {
+ val caseWhen = CaseWhen(
+ Seq((cond, base)),
+ Some(Literal(null)))
+ Count(caseWhen).toAggregateExpression(isDistinct = true)
+ }
+
+ private def collectAggregateExpressions(plan: LogicalPlan):
Seq[AggregateExpression] = {
+ plan.collect { case a: Aggregate => a.aggregateExpressions }
+ .flatten
+ .flatMap(_.collect { case ae: AggregateExpression => ae })
+ }
+
+ /**
+ * Asserts that the optimized plan has exactly one Expand node with one
projection,
+ * that the shared distinct key is guarded by the union of the member
filters,
+ * and that the outer aggregate has moved the conditions into FILTER clauses.
+ */
+ private def assertSingleDistinctGroupExpand(
+ optimized: LogicalPlan,
+ baseColName: String,
+ conds: Seq[Expression]): Unit = {
+ // rewrite() adds EqualTo(gid, id) to every distinct aggregate, so
filter.isDefined
+ // alone always passes. Require a user-condition reference (same as
assertCanonicalized).
+ val hasUserConditionFilter = collectAggregateExpressions(optimized)
+ .flatMap(_.filter)
+ .exists(_.references.exists(_.name != "gid"))
+ assert(hasUserConditionFilter,
+ "expected at least one AggregateExpression with a user-condition FILTER
clause")
+ val baseAttr = conditionalTestRelation.output.find(_.name ==
baseColName).get
+ assertGuardedExpandKey(optimized, baseAttr, conds)
+ }
+
+ /**
+ * Asserts that the collapsed distinct group's Expand key is guarded by the
union
+ * of the member filters, i.e. IF(Or(cond1, ..., condN), base, NULL): rows
matching
+ * no predicate project NULL as the distinct key and collapse into a single
key in
+ * the first aggregate, keeping its output cardinality bounded by the
pre-rewrite
+ * plan (distinct(base | Or(conds)) + 1 <= sum_i distinct(base | cond_i) +
N).
+ */
+ private def assertGuardedExpandKey(
+ optimized: LogicalPlan,
+ base: Expression,
+ conds: Seq[Expression]): Unit = {
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.size == 1,
+ s"expected 1 distinct group but got ${expand.projections.size}")
+ val guard = expand.projections.head.collectFirst {
+ case i @ If(_, trueValue, falseValue)
+ if falseValue == nullInt && trueValue.semanticEquals(base) => i
+ }.getOrElse(fail(s"no union guard over $base in Expand
projections:\n$optimized"))
+ val leaves = orLeaves(guard.predicate)
+ assert(leaves.size == conds.size,
+ s"guard condition should cover all ${conds.size} filters:
${guard.predicate}")
+ conds.foreach { c =>
+ assert(leaves.exists(_.semanticEquals(c)),
+ s"guard condition misses $c: ${guard.predicate}")
+ }
+ }
+
+ private def orLeaves(expr: Expression): Seq[Expression] = expr match {
+ case Or(left, right) => orLeaves(left) ++ orLeaves(right)
+ case other => Seq(other)
+ }
+
+ /** EXISTS / IN / NOT IN nodes in Expand projections. The guard must not
copy these. */
+ private def subqueryExprCount(plan: LogicalPlan): Int = {
+ val exprs = plan.collect { case e: Expand => e
}.flatMap(_.projections.flatten)
+ exprs.map(_.collect { case _: Exists | _: InSubquery => () }.size).sum
+ }
+
+ test("conditional: disabled when config is false") {
+ withSQLConf(SQLConf.REWRITE_COUNT_DISTINCT_CONDITIONAL_ENABLED.key ->
"false") {
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ // The general RewriteDistinctAggregates still fires, but conditional
+ // canonicalization is disabled so the two conditional counts stay as 2
+ // distinct groups instead of collapsing to 1.
+ val expands = optimized.collect { case e: Expand => e }
+ assert(expands.head.projections.size == 2,
+ "expected 2 distinct groups when conditional canonicalization is
disabled")
+ }
+ }
+
+ test("conditional: rewrite COUNT(DISTINCT IF(cond, col, NULL)) to
COUNT(DISTINCT col) FILTER") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertSingleDistinctGroupExpand(optimized, "c", Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: rewrite COUNT(DISTINCT CASE WHEN cond THEN col END) to " +
+ "COUNT(DISTINCT col) FILTER") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctCaseWhen(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctCaseWhen(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertSingleDistinctGroupExpand(optimized, "c", Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: rewrite COUNT(DISTINCT CASE WHEN cond THEN col ELSE NULL
END)") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctCaseWhenElseNull(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctCaseWhenElseNull(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertSingleDistinctGroupExpand(optimized, "c", Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: multiple conditional distinct counts collapse to single
distinct group") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"),
+ countDistinctIf(Symbol("b") > 3, Symbol("c")).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ // All three counts share the same base column c, collapsed to 1 distinct
group.
+ assertSingleDistinctGroupExpand(
+ optimized, "c", Seq(bAttr > 1, bAttr > 2, bAttr > 3))
+ }
+
+ test("conditional: guarded Expand key is IF(union of conditions, base,
NULL)") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertGuardedExpandKey(optimized, base, Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: guarded Expand key covers CASE WHEN conditions too") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctCaseWhen(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctCaseWhenElseNull(Symbol("b") > 2, Symbol("c")).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertGuardedExpandKey(optimized, base, Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: guarded Expand key covers all conditions of a 3-count
collapse") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"),
+ countDistinctIf(Symbol("b") > 3, Symbol("c")).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertGuardedExpandKey(optimized, base, Seq(bAttr > 1, bAttr > 2, bAttr >
3))
+ }
+
+ test("conditional: user-written FILTER counts sharing a base get the union
guard") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(bAttr > 1)).as("cnt1"),
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(bAttr > 2)).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ assertGuardedExpandKey(optimized, base, Seq(bAttr > 1, bAttr > 2))
+ }
+
+ test("conditional: config false disables union guard for user-written
FILTER") {
+ // The config is the kill switch for both IF/CASE normalize and the
union-of-filters
+ // guard. With it off, user-written FILTER distincts still Expand
(mustRewrite) but
+ // keep the raw base as the Expand key.
+ withSQLConf(SQLConf.REWRITE_COUNT_DISTINCT_CONDITIONAL_ENABLED.key ->
"false") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(bAttr > 1))
+ .as("cnt1"),
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(bAttr > 2))
+ .as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.size == 1)
+ assert(expand.projections.head.exists(_.semanticEquals(base)),
+ "raw base expected when rewriteCountDistinctConditional is disabled")
+ assert(
+ !expand.projections.head.exists {
+ case If(_, trueValue, falseValue) =>
+ falseValue == nullInt && trueValue.semanticEquals(base)
+ case _ => false
+ },
+ "union guard should be off when rewriteCountDistinctConditional is
disabled")
+ }
+ }
+
+ test("conditional: unfiltered count in the group keeps the raw base key") {
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(base).toAggregateExpression(isDistinct = true).as("cnt0"),
+ Count(base).toAggregateExpression(isDistinct = true,
+ filter = Some(Symbol("b") > 1)).as("cnt1"),
+ Count(base).toAggregateExpression(isDistinct = true,
+ filter = Some(Symbol("b") > 2)).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ // One distinct group {c}, but the unfiltered count needs the raw child
over all
+ // rows, so the Expand key must not be guarded.
+ assert(expand.projections.size == 1)
+ assert(expand.projections.head.exists(_.semanticEquals(base)),
+ "raw base attribute expected when the group contains an unfiltered
count")
+ }
+
+ test("conditional: non-deterministic filter keeps the raw base key") {
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val nondet = Rand(42) < Literal(0.5, DoubleType)
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(nondet)).as("cnt1"),
+ Count(base).toAggregateExpression(isDistinct = true, filter =
Some(nondet)).as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ // The guard re-evaluates the filters, so non-deterministic ones must not
be
+ // guarded; the Expand key stays the raw attribute.
+ assert(expand.projections.head.exists(_.semanticEquals(base)),
+ "raw base attribute expected with a non-deterministic filter")
+ }
+
+ test("conditional: EXISTS, IN, and NOT IN filters keep the raw key") {
+ // RewritePredicateSubquery has not run. Copying a predicate subquery into
the
+ // Expand key builds a second ExistenceJoin later, so the key stays raw
and each
+ // predicate appears once, as its flag column.
+ val dim = LocalRelation(Symbol("x").int)
+ val existsPred = Exists(dim)
+ val inPred = InSubquery(Seq(Symbol("b")), ListQuery(dim, numCols = 1))
+ val notInPred = Not(InSubquery(Seq(Symbol("c")), ListQuery(dim, numCols =
1)))
+ val b = conditionalTestRelation.output.find(_.name == "b").get
+ val c = conditionalTestRelation.output.find(_.name == "c").get
+ val d = conditionalTestRelation.output.find(_.name == "d").get
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(b).toAggregateExpression(isDistinct = true, filter =
Some(existsPred)).as("cnt1"),
+ Count(c).toAggregateExpression(isDistinct = true, filter =
Some(inPred)).as("cnt2"),
+ Count(d).toAggregateExpression(isDistinct = true, filter =
Some(notInPred)).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.size == 3)
+ Seq(b, c, d).foreach { base =>
+ val nullOfBase = Literal.create(null, base.dataType)
+ assert(expand.projections.exists(_.exists(_.semanticEquals(base))),
+ s"raw key expected for $base")
+ assert(!expand.projections.exists(_.exists {
+ case If(_, trueValue, falseValue) =>
+ falseValue.semanticEquals(nullOfBase) &&
trueValue.semanticEquals(base)
+ case _ => false
+ }), s"subquery filter must not guard $base")
+ }
+ assert(subqueryExprCount(optimized) == 3,
+ "each predicate subquery should appear once in the Expand")
+ }
+
+ test("conditional: do not canonicalize counts whose condition is a predicate
subquery") {
+ // Normalizing would merge these into one group that cannot be guarded,
and Expand
+ // would project the raw base. Leave each IF/CASE in its own group.
+ val dim = LocalRelation(Symbol("x").int)
+ val x = dim.output.head
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Exists(dim.where(x > 0)), Symbol("c")).as("cnt1"),
+ countDistinctCaseWhen(Exists(dim.where(x > 1)),
Symbol("c")).as("cnt2"),
+ countDistinctCaseWhenElseNull(Exists(dim.where(x > 2)),
Symbol("c")).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.size == 3,
+ "subquery conditions should stay 3 distinct groups")
+ val hasUserConditionFilter = collectAggregateExpressions(optimized)
+ .flatMap(_.filter)
+ .exists(_.references.exists(_.name != "gid"))
+ assert(!hasUserConditionFilter,
+ "subquery condition should not become a user-condition FILTER clause")
+ assert(subqueryExprCount(optimized) == 3,
+ "each predicate subquery should appear once in the Expand")
+ }
+
+ test("conditional: subquery-free filters stay guarded beside a subquery
condition") {
+ val bAttr = conditionalTestRelation.output.find(_.name == "b").get
+ val base = conditionalTestRelation.output.find(_.name == "c").get
+ val dim = LocalRelation(Symbol("x").int)
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"),
+ countDistinctIf(Symbol("b") > 2, Symbol("c")).as("cnt2"),
+ countDistinctIf(Exists(dim), Symbol("c")).as("cnt3"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ // The two plain predicates share one guarded group. The EXISTS count
stays separate.
+ assert(expand.projections.size == 2)
+ val guard = expand.projections.flatten.collectFirst {
+ case i @ If(_, trueValue, falseValue)
+ if falseValue == nullInt && trueValue.semanticEquals(base) => i
+ }.getOrElse(fail(s"no union guard over c:\n$optimized"))
+ val leaves = orLeaves(guard.predicate)
+ assert(leaves.size == 2, s"guard should cover only the plain predicates:
${guard.predicate}")
+ Seq(bAttr > 1, bAttr > 2).foreach { cond =>
+ assert(leaves.exists(_.semanticEquals(cond)), s"guard misses $cond:
${guard.predicate}")
+ }
+ assert(subqueryExprCount(optimized) == 1,
+ "the EXISTS condition should appear once, in its own group")
+ }
+
+ test("conditional: throwable distinct child keeps the raw key") {
+ // Division can throw under ANSI on rows the filter rejects. Wrapping it in
+ // the guard would skip those evaluations, so the Expand key stays raw.
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(Symbol("c") / Symbol("b")).toAggregateExpression(
+ isDistinct = true, filter = Some(Symbol("a") > 0)).as("cnt"))
+ .analyze
+ val child = input.collectFirst {
+ case a: Aggregate =>
+ a.aggregateExpressions.flatMap(_.collect {
+ case ae: AggregateExpression if ae.isDistinct =>
+ ae.aggregateFunction.children.headOption
+ }).flatten.head
+ }.get
+ val optimized = RewriteDistinctAggregates(input)
+ val expand = optimized.collectFirst { case e: Expand => e }.get
+ assert(expand.projections.head.exists(_.semanticEquals(child)),
+ s"raw distinct child expected, projections:\n${expand.projections.head}")
+ }
+
+ test("conditional: single conditional distinct count is gated out by
mayNeedtoRewrite") {
+ // A lone COUNT(DISTINCT IF(...)) must NOT be pushed onto the Expand path.
+ // The canonicalization runs inside rewrite(), which is only called when
+ // mayNeedtoRewrite returns true. A single conditional distinct count has
no filter
+ // and forms only one distinct group, so mayNeedtoRewrite returns false,
rewrite()
+ // is never called, and no Expand is produced.
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt1"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ val expands = optimized.collect { case e: Expand => e }
+ assert(expands.isEmpty, "single conditional distinct count should not
produce an Expand")
+ }
+
+ test("conditional: COUNT+SUM sharing IF keeps single-distinct path") {
+ // Both share one unfiltered distinct child. Normalizing only the COUNT
would
+ // split the group into {c} and {IF(b>1,c,NULL)} and force a 2-projection
Expand.
+ // Keep the original aggregate so AggregationStrategy handles it without
Expand.
+ // Use the same null literal as countDistinctIf so the IF children are one
+ // ExpressionSet key after analysis (as in SQL COUNT/SUM over the same IF).
+ val sharedIf = If(Symbol("b") > 1, Symbol("c"), Literal(null))
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(sharedIf).toAggregateExpression(isDistinct = true).as("cnt"),
+ Sum(sharedIf).toAggregateExpression(isDistinct = true).as("sum"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ comparePlans(optimized, input)
+ }
+
+ test("conditional: repeated COUNT(DISTINCT IF) with COUNT(*) keeps
single-distinct path") {
+ // Two identical conditional counts form one unfiltered distinct group.
Normalizing
+ // them to FILTER form would make mustRewrite true and introduce an Expand
+ // (regular branch + distinct branch) with no fan-out reduction.
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt"),
+ countDistinctIf(Symbol("b") > 1, Symbol("c")).as("cnt2"),
+ Count(Literal(1)).toAggregateExpression().as("n"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ comparePlans(optimized, input)
+ }
+
+ test("conditional: do not rewrite IF with non-null else branch") {
+ val input = conditionalTestRelation
+ .groupBy(Symbol("a"))(
+ Count(If(Symbol("b") > 1, Symbol("c"), Literal(0, IntegerType)))
+ .toAggregateExpression(isDistinct = true)
+ .as("cnt1"),
+ Count(If(Symbol("b") > 2, Symbol("c"), Literal(0, IntegerType)))
+ .toAggregateExpression(isDistinct = true)
+ .as("cnt2"))
+ .analyze
+ val optimized = RewriteDistinctAggregates(input)
+ // Plan is rewritten (2 distinct groups) but not canonicalized
+ checkRewrite(optimized)
+ val expands = optimized.collect { case e: Expand => e }
+ assert(expands.head.projections.size == 2,
+ "non-null else branch should not be collapsed to 1 distinct group")
+ }
+
+ test("conditional: do not rewrite non-distinct COUNT") {
Review Comment:
**Nit (P3):** With one distinct aggregate (`countDistinct(c)`) and no
FILTER, `mayNeedtoRewrite` returns false, so `rewrite()` and
`normalizeCountDistinctConditional` never run, and `comparePlans(optimized,
input)` passes no matter what the pattern does with a non-distinct
`COUNT(IF(...))`. Adding only a second distinct aggregate on another column
wouldn't help either, because the strict-reduction gate discards a
normalization that doesn't reduce the distinct-group count. If the fixture
paired the non-distinct COUNT with two conditional distinct counts on one base,
so that normalization is kept, and asserted that the non-distinct COUNT still
has its `IF` child and no user-condition FILTER, the check could actually fail.
**Verification:**
- **Regression:** The optimized plan shows the conditional distinct counts
collapsed into a guarded group while the non-distinct COUNT keeps its IF child
and has no user-condition FILTER.
- **Inspection:** The fixture reaches rewrite() with normalization kept, so
dropping the isDistinct restriction from the pattern would change the asserted
plan.
--
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]