Skip to content

fix(physical-plan): honor distinct soft limits in SingleHashAggregateStream - #25158

Open
TinyMurky wants to merge 1 commit into
apache:mainfrom
TinyMurky:SingleHashAggregateStream-add-soft-limit
Open

fix(physical-plan): honor distinct soft limits in SingleHashAggregateStream#25158
TinyMurky wants to merge 1 commit into
apache:mainfrom
TinyMurky:SingleHashAggregateStream-add-soft-limit

Conversation

@TinyMurky

@TinyMurky TinyMurky commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

SingleHashAggregateStream ignores the distinct soft limit pushed into AggregateExec. As a result, single-stage SELECT DISTINCT ... LIMIT n queries consume all input even after enough distinct groups have been collected.

This change stops input consumption once the in-memory hash table contains at least the requested number of distinct groups. Any existing spills are merged before producing output, and the downstream limit operator enforces the exact output row count.

What changes are included in this PR?

  • Add a distinct soft-limit check to SingleHashAggregateStream after processing each input batch.
  • Reuse the input-exhausted transition when the soft limit is reached, preserving spill merging and output preparation.
  • Add unit tests covering early termination with and without spilling.
    • Update the AggregateExec::limit_options documentation to list SingleHash as supporting distinct soft limits.

Are these changes tested?

Added unit tests covering:

  • Reaching the soft limit without spilling.
  • Reaching the soft limit after spilling, preserving spilled groups and deduplicating overlapping groups.
  • Rejecting further input consumption after the soft limit is reached in the spill case.

Following test commands have been executed and passed

  • cargo test --profile=ci --test sqllogictests
  • cargo test -p datafusion
  • cargo test -p datafusion-cli

Are there any user-facing changes?

Single-stage DISTINCT queries with a limit can stop consuming input earlier, reducing unnecessary work. Query semantics are unchanged.

…Stream

## Which issue does this PR close?

- Closes [apache#24980](apache#24980)

## Rationale for this change

`SingleHashAggregateStream` ignores the distinct soft limit pushed into
`AggregateExec`. As a result, single-stage `SELECT DISTINCT ... LIMIT n`
queries consume all input even after enough distinct groups have been
collected.

This change stops input consumption once the in-memory hash table
contains at least the requested number of distinct groups. Any existing
spills are merged before producing output, and the downstream limit
operator enforces the exact output row count.

## What changes are included in this PR?

* Add a distinct soft-limit check to `SingleHashAggregateStream` after
processing each input batch.
* Reuse the input-exhausted transition when the soft limit is reached,
preserving spill merging and output preparation.
* Add unit tests covering early termination with and without spilling.
  - Update the `AggregateExec::limit_options` documentation to list
`SingleHash` as supporting distinct soft limits.

## Are these changes tested?

Added unit tests covering:

* Reaching the soft limit without spilling.
* Reaching the soft limit after spilling, preserving spilled groups and
deduplicating overlapping groups.
* Rejecting further input consumption after the soft limit is reached in
the spill case.

Following test commands have been executed and passed

- `cargo test --profile=ci --test sqllogictests`
- `cargo test -p datafusion`
- `cargo test -p datafusion-cli`

## Are there any user-facing changes?

Single-stage `DISTINCT` queries with a limit can stop consuming input
earlier, reducing unnecessary work. Query semantics are unchanged.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.12346% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.90%. Comparing base (5747e87) to head (f7a7acb).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/physical-plan/src/aggregates/mod.rs 88.18% 6 Missing and 9 partials ⚠️
...sion/physical-plan/src/aggregates/single_stream.rs 97.14% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25158      +/-   ##
==========================================
- Coverage   81.90%   81.90%   -0.01%     
==========================================
  Files        1132     1132              
  Lines      420155   420296     +141     
  Branches   420155   420296     +141     
==========================================
+ Hits       344126   344239     +113     
- Misses      55742    55756      +14     
- Partials    20287    20301      +14     

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

@2010YOUY01 2010YOUY01 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.

Thank you for working on this. I've left several suggestions.

return Self::break_with_err(e);
}

// Soft limit optimization:

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.

Can we move this comment to SingleHashAggregateStream, and here we can comment 'see comments at xxx for details'

Additionally we can follow the comment pattern in (first explain how the SQL get optimized to soft limit, and next the internal early termination mechanism)

// Reuse the input-exhausted transition to merge any existing spills
// before producing output. The downstream limit operator enforces
// the exact output row count.
if self.hit_soft_group_limit(&hash_table) {

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.

I suggest to skip this optimization if we have spilled before

Here is the pattern to follow, and also the explanaiton

// Soft group limits are usually small and rarely coincide with
// spilling. Once spilling has occurred, skip this optimization to
// make the internal logic simpler.
let spilled = spill_context
.as_ref()
.is_some_and(|context| context.has_spills());
if self.hit_soft_group_limit(hash_table) && !spilled {
break;
}

return Self::break_with_err(e);
}

// Soft limit optimization:

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.

and we can update control flow comment at poll_next to briefly mention this optimization change

Ok(())
}

#[tokio::test]

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.

I recommend to write this test differently (follow the pattern in 40a6454#diff-02af0439a3df656429990b220b80e50d8df259ce45c47e008460c1ca3781aca3)

The main difference is

  • Try to exercise this feature end-to-end, from select distinct query, and get it optimized to aggregate with soft limit
  • Also assert the internal metric of AggregateExec, otherwise we can't ensure if this soft limit optimization is applied -- limit can also be enforced by the downstream LimitExec operator.

(I think only such e2e test is enough, we don't have to test it individually on AggregateExec, since this optimization is only useful from such SQL patterns, and should not be directly used on the AggregateExec)

Comment on lines +108 to +109
/// When set, there are no aggregate expressions: AggregateExec routes
/// limited non-DISTINCT aggregates to a different stream.

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.

Maybe we can also change it to 'see top comments for details'.

@TinyMurky

Copy link
Copy Markdown
Contributor Author

Thanks for the review!
I will start to work on them!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-plan Changes to the physical-plan crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SingleHashAggregateStream ignores the distinct soft limit (lim=[n]) and reads all input

3 participants