peter-toth commented on code in PR #58106:
URL: https://github.com/apache/spark/pull/58106#discussion_r3814887092


##########
sql/core/src/test/scala/org/apache/spark/sql/execution/aggregate/AdaptivePartialAggregationSuite.scala:
##########
@@ -198,16 +208,16 @@ class AdaptivePartialAggregationSuite extends QueryTest 
with SharedSparkSession
 
   private def numBypassingRows(build: () => DataFrame): Long = 
runAndReadCounters(build).skipped
 
-  // Returns the bypassed-row count per Partial-mode `HashAggregateExec`, 
keyed by the number of
+  // Returns the bypassed-row count per partial `HashAggregateExec` phase, 
keyed by the number of
   // grouping keys, and verifies the run matches the feature-off reference. A 
`count(DISTINCT ...)`
-  // group-by has two such Partial phases -- the de-duplication partial 
(grouping on key + distinct
-  // columns) and the distinct partial (grouping on the keys only) -- so their 
bypasses can be told
-  // apart by the grouping key count.
+  // group-by has two such phases -- the de-duplication partial (grouping on 
key + distinct
+  // columns) and the distinct partial (grouping on the keys only, whose 
non-distinct aggregates run
+  // in `PartialMerge`) -- so their bypasses can be told apart by the grouping 
key count.
   private def bypassRowsByGroupingKeyCount(build: () => DataFrame): Map[Int, 
Long] = {
     val df = build()
     df.collect()
     val byKeyCount = collect(df.queryExecution.executedPlan) {
-      case agg: HashAggregateExec if agg.aggregateExpressions.forall(_.mode == 
Partial) =>
+      case agg: HashAggregateExec if isPartialPhase(agg) =>

Review Comment:
   **Finding 2.** With the predicate widened, the pure-`PartialMerge` 
de-duplication phase now matches `isPartialPhase` and lands in the same 
grouping-key bucket as the leading `Partial` phase -- both group on `(k, v)`. 
So the one phase the new `exists(_.mode == Partial)` guard exists to exclude 
has no observable count of its own: if it ever became eligible, its bypasses 
would be summed into `byKeyCount(2)` and the `> 0` assertions would still pass. 
`checkAgainstReference` would catch the resulting over-counted DISTINCT, but 
nothing pins the eligibility rule itself.
   
   The helper comment above already states that such a phase registers no 
metric, so asserting it is a couple of lines here:
   
   ```scala
       val dedupPhases = collect(df.queryExecution.executedPlan) {
         case agg: HashAggregateExec if agg.aggregateExpressions.nonEmpty &&
           agg.aggregateExpressions.forall(_.mode == PartialMerge) => agg
       }
       assert(dedupPhases.forall(!_.metrics.contains("numBypassingRows")),
         "the pure-PartialMerge de-duplication phase must stay ineligible")
   ```
   
   It holds on this head: that phase reports no `numBypassingRows` metric while 
the `PartialMerge ++ Partial` phase reports 392 in the new test.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/aggregate/AdaptivePartialAggregationSuite.scala:
##########
@@ -477,6 +487,62 @@ class AdaptivePartialAggregationSuite extends QueryTest 
with SharedSparkSession
     }
   }
 
+  test("distinct with plain and filtered non-distinct aggregates") {
+    // One query carries all three shapes through the DISTINCT intermediate 
phase
+    // (`PartialMerge ++ Partial`): the distinct aggregate (`count(DISTINCT 
v)`), a plain
+    // non-distinct aggregate (`sum(v)`), and a filtered non-distinct aggregate
+    // (`avg(v) FILTER (...)`, whose `FILTER` is applied in the leading 
`Partial` phase only).
+    // Fully distinct keys and values make neither partial phase reduce 
anything, so both bypass in
+    // the same execution: asserting the 2-key phase (de-duplication, all 
`Partial`) and the 1-key
+    // phase (distinct partial, `PartialMerge ++ Partial`) together proves the 
two bypasses coexist,
+    // the plain and filtered non-distinct buffers pass through correctly, and 
the results still
+    // match the feature-off reference.
+    withTempView("t") {
+      spark.range(0, 400, 1, 1)

Review Comment:
   **Finding 1.** A one-slice `RangeExec` reports `SinglePartition`, which 
satisfies every `ClusteredDistribution`, so `EnsureRequirements` inserts no 
`Exchange` anywhere and the whole four-phase DISTINCT plan fuses into one 
stage. Dumped on this head:
   
   ```
   *(1) HashAggregate(keys=[k], functions=[sum(v), avg(v), count(distinct v)])
   +- *(1) HashAggregate(keys=[k], functions=[merge_sum(v), merge_avg(v), 
partial_count(distinct v)])
      +- *(1) HashAggregate(keys=[k, v], functions=[merge_sum(v), merge_avg(v)])
         +- *(1) HashAggregate(keys=[k, v], functions=[partial_sum(v), 
partial_avg(v) FILTER (WHERE (v > 25))])
            +- *(1) Project [cast(id as string) AS k, id AS v]
               +- *(1) Range (0, 400, step=1, splits=1)
   ```
   
   Fused, this operator's `outputFunc` feeds the next aggregate's `doConsume`, 
so nothing reaches `BufferedRowIterator.currentRows`, `shouldStop()` stays 
false for the whole build, and the first bypassed row drains the entire frozen 
map in a single `outputMapAndFlush()` call. The interesting half of the 
machinery -- one map row per queued row, the queue flush after the map drains, 
and the `adaptiveResumeBuild` re-entry -- only runs with an `Exchange` above 
the operator, which is also the shape where the merge-order work on #57742 
lived. Neither new test reaches it, and neither carries an order-sensitive 
non-distinct aggregate, which is the only place the pass-through's 
merge-into-an-empty-buffer step is observable.
   
   `distinct aggregation stays correct` (`:481`) does reach the split shape for 
this phase at `parts = 2` -- I confirmed both exchanges appear and the 
`PartialMerge ++ Partial` phase bypasses 292 rows there -- but its 
`expectBypass` assertion sums `numBypassingRows` over every matching phase, so 
the leading `Partial` phase alone satisfies it.
   
   Suggested shape: vary the partition count and add an order-sensitive member.
   
   ```scala
       forEachCodegenAndMap() { clue =>
         Seq(1, 2).foreach { parts =>
           val df = () => spark.range(0, 400, 1, parts)
             .select(($"id" % 100).cast("string") as "k", ($"id" % 7) as "v", 
$"id" as "w")
             .groupBy($"k")
             .agg(countDistinct($"v") as "cd", sum($"w") as "s",
               first($"w") as "f", last($"w") as "l")
           ...
         }
       }
   ```
   
   Deriving both `k` and `v` matters: with `v = id` the `Project` keeps the 
`Range`'s partitioning as `RangePartitioning(v)`, which satisfies 
`ClusteredDistribution(k, v)`, and the exchange is skipped again even at `parts 
= 2`.
   
   I ran that query and it passes today (splits 1 and 2, codegen on and off, 
two-level map on and off), so this is coverage rather than a live bug. One 
caveat if you add `first`/`last`: keep the forced-`testFallbackStartsAt` cells 
out of the reference comparison. The sort-based fallback reorders 
`first`/`last` in the *reference* arm too -- feature off, that query returns 
`(f, l) = (210, 110)` for `k = 10` with `testFallbackStartsAt = "4, 16"` and 
`(10, 310)` without it -- so a forced-spill cell would compare two legitimately 
different orders.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/aggregate/AdaptivePartialAggregationSuite.scala:
##########
@@ -477,6 +487,62 @@ class AdaptivePartialAggregationSuite extends QueryTest 
with SharedSparkSession
     }
   }
 
+  test("distinct with plain and filtered non-distinct aggregates") {
+    // One query carries all three shapes through the DISTINCT intermediate 
phase
+    // (`PartialMerge ++ Partial`): the distinct aggregate (`count(DISTINCT 
v)`), a plain
+    // non-distinct aggregate (`sum(v)`), and a filtered non-distinct aggregate
+    // (`avg(v) FILTER (...)`, whose `FILTER` is applied in the leading 
`Partial` phase only).
+    // Fully distinct keys and values make neither partial phase reduce 
anything, so both bypass in
+    // the same execution: asserting the 2-key phase (de-duplication, all 
`Partial`) and the 1-key
+    // phase (distinct partial, `PartialMerge ++ Partial`) together proves the 
two bypasses coexist,
+    // the plain and filtered non-distinct buffers pass through correctly, and 
the results still
+    // match the feature-off reference.
+    withTempView("t") {
+      spark.range(0, 400, 1, 1)
+        .select($"id".cast("string") as "k", $"id" as "v")
+        .createOrReplaceTempView("t")
+      forEachCodegenAndMap() { clue =>
+        val df = () => spark.sql(
+          """SELECT k,
+            |       count(DISTINCT v) AS cd,
+            |       sum(v) AS s,
+            |       avg(v) FILTER (WHERE v > 25) AS a_gt25
+            |FROM t GROUP BY k""".stripMargin)
+        withClue(clue) {
+          val byKeyCount = bypassRowsByGroupingKeyCount(df)
+          assert(byKeyCount.get(2).exists(_ > 0),
+            s"expected the de-duplication partial (grouping on k, v) to 
bypass, got $byKeyCount")
+          assert(byKeyCount.get(1).exists(_ > 0),
+            s"expected the distinct partial (PartialMerge++Partial, grouping 
on k) to bypass, " +
+              s"got $byKeyCount")
+        }
+      }
+    }
+  }
+
+  test("an imperative aggregate stays correct in the distinct intermediate 
phase") {
+    // `approx_count_distinct` uses `HyperLogLogPlusPlus`, an 
`ImperativeAggregate` whose buffer is
+    // written by `initialize`/`merge` rather than a projection, so the 
distinct intermediate phase
+    // runs on `TungstenAggregationIterator` (supportCodegen = false). 
Asserting both phases bypass
+    // -- the de-duplication partial (grouping on `k` + `v`) and the distinct 
partial (grouping on
+    // `k`, whose `PartialMerge` member is imperative) -- proves the 
imperative buffer is reset then
+    // merged with the incoming buffer on pass-through, and the results still 
match the reference.
+    forEachCodegenAndMap() { clue =>
+      val df = () => spark.range(0, 400, 1, 1)
+        .select(($"id" % 50).cast("string") as "k", ($"id" % 20) as "v", $"id" 
as "x")
+        .groupBy($"k")
+        .agg(approx_count_distinct($"x") as "acd", countDistinct($"v") as "cd")
+      withClue(clue) {
+        val byKeyCount = bypassRowsByGroupingKeyCount(df)
+        assert(byKeyCount.get(2).exists(_ > 0),
+          s"expected the de-duplication partial (grouping on k, v) to bypass, 
got $byKeyCount")
+        assert(byKeyCount.get(1).exists(_ > 0),
+          s"expected the distinct partial (imperative PartialMerge, grouping 
on k) to bypass, " +
+            s"got $byKeyCount")
+      }
+    }
+  }
+
   test("distinct aggregation bypasses on high-cardinality input") {
     // The `PartialMerge` phase of the multi-phase distinct plan always 
aggregates (it is not
     // `Partial` mode and requires a distribution), so the rows reaching the 
distinct `Partial`

Review Comment:
   **Finding 3.** "it is not `Partial` mode" stops being a reason with this PR 
-- `PartialMerge` modes are eligible now. And for this test's query 
(`countDistinct($"v")` with no non-distinct aggregate) that phase has no 
aggregate expressions at all, so it is a group-by-only aggregate kept out 
purely by its required distribution. Suggest dropping the mode half:
   
   ```scala
       // The `PartialMerge` phase of the multi-phase distinct plan always 
aggregates (it requires a
       // distribution, so it is never eligible), so the rows reaching the 
distinct `Partial` phase
       // are de-duplicated and pass-through carries exactly one distinct value 
each.
   ```
   



-- 
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]

Reply via email to