Skip to content

refactor: Make AggregateExec state modeling and public updates safe (part 1: distinct limit) - #25564

Merged
2010YOUY01 merged 5 commits into
apache:mainfrom
2010YOUY01:distinct-aggregate
Sep 23, 2026
Merged

2010YOUY01 merged 5 commits into
apache:mainfrom
2010YOUY01:distinct-aggregate

Conversation

@2010YOUY01

@2010YOUY01 2010YOUY01 commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Which issue does this PR close?

Rationale for this change

See issue for the idea, this PR is the step 1 in the issue.

For backgrounds for distinct limit optimization (this PR targets), see

/// # Optimization: DISTINCT LIMIT Soft Limit

What changes are included in this PR?

  1. Refactor AggregateExec with enum state
  2. Add safe API for this optimization, and use it in the optimizer.
    (unsafe one will be able to be deleted after step2, now it's reused by other path)

I'll mark the structural changes (only 2 files)

Most diff is mechanical changes like aggregate_exec.aggr_expr -> aggregate_exec.aggr_expr(), caused by the refactor.

What is the testing strategy for this PR?

Existing tests.

Are there any user-facing changes?

no

@github-actions github-actions Bot added optimizer Optimizer rules physical-plan Changes to the physical-plan crate labels Sep 21, 2026
use std::sync::Arc;

use datafusion_physical_plan::aggregates::{AggregateExec, LimitOptions};
use datafusion_physical_plan::aggregates::AggregateExec;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structural change 1

use arrow_schema::FieldRef;
use datafusion_common::stats::Precision;
use datafusion_common::tree_node::TreeNodeRecursion;
use datafusion_common::tree_node::{Transformed, TreeNodeRecursion};

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file includes structural change 2.

Remaning diffs are all mechanical access method change

@codecov-commenter

codecov-commenter commented Sep 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.75635% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.42%. Comparing base (2424dae) to head (e01af79).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/physical-plan/src/aggregates/mod.rs 82.99% 23 Missing and 2 partials ⚠️
datafusion/physical-plan/src/aggregates/spill.rs 33.33% 0 Missing and 2 partials ⚠️
...ical-optimizer/src/limited_distinct_aggregation.rs 88.88% 0 Missing and 1 partial ⚠️
...plan/src/aggregates/aggregate_hash_table/common.rs 80.00% 0 Missing and 1 partial ⚠️
.../aggregates/aggregate_hash_table/common_ordered.rs 80.00% 0 Missing and 1 partial ⚠️
...n/physical-plan/src/aggregates/aggregate_stream.rs 80.00% 0 Missing and 1 partial ⚠️
...hysical-plan/src/aggregates/grouped_topk_stream.rs 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25564      +/-   ##
==========================================
- Coverage   82.42%   82.42%   -0.01%     
==========================================
  Files        1139     1139              
  Lines      435410   435467      +57     
  Branches   435410   435467      +57     
==========================================
+ Hits       358884   358922      +38     
- Misses      54822    54838      +16     
- Partials    21704    21707       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @2010YOUY01 this makes sense to me. I had a few concerns about some extra cloning and filter reset, but the basic pattern looks great to me. I like where this is heading

FYI @rluvaton @Rachelint and @jayzhan211 who I think are also working in this area

let new_aggr = aggr.with_new_limit_options(Some(LimitOptions::new(limit)));

Some(Arc::new(new_aggr))
let new_aggr = aggr.clone().try_optimize_distinct_soft_limit(limit).ok()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern of ignoring Errs I think is bad in DataFusion because each Err contains an allocated string (so this is inefficient).

Perhaps we could just change try_optimize_distinct_soft_limit to return an Option rather than Result 🤔

Also, it would be great to avoid the need to clone the input(as this is a deep clone, not just a an arc clone)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Option instead of Result: I agree, updated in 2cf27d8
  • Avoiding deep clone: It seems impossible due to the optimizer interface restriction?

rewrite_applicable = false;
Ok(Transformed::no(plan))
};
let child = child.to_owned().transform_down(closure).data().ok()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though here is an example of discarding an error in existing code too 😬

///
/// The remaining streams consume all input.
limit_options: Option<LimitOptions>,
kind: AggregateKind,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

mut self,
limit: usize,
) -> Result<Transformed<Self>> {
if limit == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to always return "Ok" -- why is it returning Result?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 2cf27d8

// An accumulator rewrite cannot inherit DISTINCT's early stop.
new.kind = AggregateKind::General {
group_by: Arc::clone(group_by),
filter_expr: vec![None; aggr_expr.len()].into(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the filter expressions reset?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous AggregateKind is DistinctLimit, which is for the query pattern select distinct k from t limit 10 after it gets optimized into an aggregate, so it must have no aggregate functions or filters.

This transformation converts DistinctLimit -> General, so there is no filter to init.

However, the deeper question seems to be: is it safe? I'm not 100% sure. Before this refactor, this conversion happened implicitly and was easy to miss. With the typed state, it now shows up obviously.

I'll add more comment for it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, the deeper question seems to be: is it safe?

Agreed

With the typed state, it now shows up obviously.

That is a neat result

@2010YOUY01
2010YOUY01 added this pull request to the merge queue Sep 23, 2026
Merged via the queue into apache:main with commit ebdb657 Sep 23, 2026
41 checks passed
@2010YOUY01
2010YOUY01 deleted the distinct-aggregate branch September 23, 2026 00:56
@adriangb

Copy link
Copy Markdown
Contributor

Thanks @2010YOUY01 this is great!

Omega359 pushed a commit to Omega359/arrow-datafusion that referenced this pull request Oct 11, 2026
… (part 1: distinct limit) (apache#25564)

## Which issue does this PR close?

<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax. For example
`Closes apache#123` indicates that this PR will close issue apache#123.
-->

- Part of apache#25563

## Rationale for this change

<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.

Please explain the problem you are trying to solve in terms of the
user-visible
behavior, rather than the implementation.

For example, "The code in `foo.rs` doesn't handle nulls" is a symptom of
the
implementation. "COUNT(DISTINCT) returns wrong results when the column
contains
nulls" is the user-visible problem.
-->
See issue for the idea, this PR is the step 1 in the issue.

For backgrounds for distinct limit optimization (this PR targets), see
https://github.com/apache/datafusion/blob/1e09a2a3d76c03f469e80bed712a6bada45e77c8/datafusion/physical-plan/src/aggregates/hash_stream.rs#L77

## What changes are included in this PR?

<!--
There is no need to duplicate the description in the issue here, but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->
1. Refactor AggregateExec with enum state
2. Add safe API for this optimization, and use it in the optimizer.
(unsafe one will be able to be deleted after step2, now it's reused by
other path)

I'll mark the structural changes (only 2 files)

Most diff is mechanical changes like `aggregate_exec.aggr_expr` ->
`aggregate_exec.aggr_expr()`, caused by the refactor.

## What is the testing strategy for this PR?

<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
4. Serve as another way to document the expected behavior of the code

Briefly describe how this PR is tested, and point to the specific tests
you added. For example: 'This new feature is covered by the
`sqllogictest` cases added in `foo.slt`'.

If this PR does not add tests, explain why. For example, if the change
is already covered by existing tests, please mention it.

You should also check the `codecov` bot reply on this PR to confirm the
changed code is exercised.
-->
Existing tests.

## Are there any user-facing changes?

<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.

If there are any breaking changes to public APIs, please add the `api
change` label.
-->
no
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

optimizer Optimizer rules physical-plan Changes to the physical-plan crate v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants