fangchenli opened a new pull request, #27080:
URL: https://github.com/apache/datafusion/pull/27080

   ## Which issue does this PR close?
   
   - Closes #23401.
   
   This picks up #23403, which the stale bot closed. It has been reworked on 
current `main`; see the note on the constant-argument guard below.
   
   ## Rationale for this change
   
   The same query gets a different plan depending on how it is written:
   
   ```rust
   // Not rewritten: the alias sits directly on the aggregate
   df.aggregate(vec![col("g")], vec![count_distinct(col("x")).alias("n")])?
   ```
   
   ```sql
   -- Rewritten into a two-phase GROUP BY
   SELECT g, count(DISTINCT x) AS n FROM t GROUP BY g
   ```
   
   `SingleDistinctToGroupBy` only matches a bare `Expr::AggregateFunction`, so 
an alias on the aggregate turns it off. DataFrame users therefore miss the 
rewrite that SQL users get, including the parallelism it provides.
   
   Measured on 4M rows and 2,000 groups, using the DataFrame API (median of 3 
rounds, each the median of 9 runs). Peak RSS includes the in-memory table:
   
   | Aggregate | Before | After | SQL, for reference | Peak RSS before → after |
   | --- | --- | --- | --- | --- |
   | `count(DISTINCT int)` | 32 ms | 34 ms | 33 ms | 0.80 → 0.82 GB |
   | `sum(DISTINCT int)` | 54 ms | 35 ms | 32 ms | 0.65 → 0.84 GB |
   | `count(DISTINCT varchar)` | 71 ms | 34 ms | 37 ms | 1.28 → 0.98 GB |
   
   The main point is that both APIs now reach the same plan. The result is not 
always an improvement: `count(DISTINCT int)` has a specialized accumulator and 
gains nothing, and `sum(DISTINCT int)` trades memory for speed, which is the 
tradeoff described in #25932. Any policy on when the rewrite should apply, such 
as #26000, is orthogonal and would now apply to both APIs equally.
   
   ## What changes are included in this PR?
   
   - `is_single_distinct_agg` looks through aliases on the aggregate, and the 
rewrite strips them. Output names are unaffected because the outer projection 
restores them from the schema.
   - The doc comment on `can_strip_every_distinct` said this rule does not 
rewrite an aliased aggregate. That is no longer true, so the comment now gives 
the reason that still applies. Its behavior does not change.
   
   An earlier version of this PR also skipped the rewrite for constant 
`DISTINCT` arguments, because the rewrite exposed a wrong result on empty input 
in `EliminateGroupByConstant`. #22132 has since fixed that, so the guard is 
gone.
   
   ## What is the testing strategy for this PR?
   
   - A DataFrame API test, `aliased_single_distinct_aggregate_is_rewritten`, 
checks the optimized plan and the results end to end.
   - Unit tests in `single_distinct_to_groupby.rs` cover aliased aggregates 
with and without `GROUP BY`, nested aliases, partly aliased aggregates, the 
case that must stay unrewritten (two distinct fields), and the `count` rollup 
path.
   
   ## Are there any user-facing changes?
   
   DataFrame queries with an aliased single `DISTINCT` aggregate now get the 
same plan as the equivalent SQL, so their performance and memory use can change 
as shown above. Results and output names do not change. There are no public API 
changes.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


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