Skip to content

feat: fix NTILE distribution logic#22051

Merged
comphead merged 2 commits intoapache:mainfrom
comphead:ntile
May 7, 2026
Merged

feat: fix NTILE distribution logic#22051
comphead merged 2 commits intoapache:mainfrom
comphead:ntile

Conversation

@comphead
Copy link
Copy Markdown
Contributor

@comphead comphead commented May 6, 2026

Which issue does this PR close?

Rationale for this change

Root cause — datafusion/functions-window/src/ntile.rs:170-176 used the linear-interpolation formula i * n / num_rows to assign bucket numbers. That formula spreads the larger buckets evenly through the partition, but the SQL standard requires the first num_rows mod n buckets to be the larger ones. For
NTILE(4) over 10 rows it produced bucket sizes 3,2,3,2 instead of 3,3,2,2

Fix — replaced the formula with the front-loaded distribution: compute base = num_rows / n and remainder = num_rows % n, treat the first remainder * (base+1) rows as the "large bucket" region (each bucket of size base+1), and the remainder as size-base buckets. The n > num_rows case (existing test
NTILE(9223377) over 3 rows) still works because base = 0 forces every row into the "large" arm where each bucket holds one row.

What changes are included in this PR?

Are these changes tested?

Are there any user-facing changes?

@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) functions Changes to functions implementation labels May 6, 2026
@comphead
Copy link
Copy Markdown
Contributor Author

comphead commented May 6, 2026

The entire CI is super fast now after #21941 thanks @blaginin

Copy link
Copy Markdown
Contributor

@coderfender coderfender left a comment

Choose a reason for hiding this comment

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

Minor comments

} else {
// base > 0 here: i >= large_rows is only reachable when remainder < n,
// which forces base >= 1 (otherwise large_rows would equal num_rows).
remainder + (i - large_rows) / base + 1
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 ! perhaps a good idea to add tests for remainder = 0 ? Like say 10 rows distributed by 5

let num_rows = num_rows as u64;
let mut vec: Vec<u64> = Vec::new();
let n = u64::min(self.n, num_rows);
let n = self.n;
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.

Should we also reject NTILE(0) directly ?

Copy link
Copy Markdown
Contributor

@mbutrovich mbutrovich left a comment

Choose a reason for hiding this comment

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

Minor issue exposed by the new code. Thanks @comphead!

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 think we need a guard for the unsigned path to match the signed path below for n == 0. The new code will panic if n is 0 in the unsigned case.

@mbutrovich
Copy link
Copy Markdown
Contributor

Good thinking, @coderfender :)

@comphead
Copy link
Copy Markdown
Contributor Author

comphead commented May 7, 2026

Thanks @mbutrovich @coderfender non valid inputs, like 0, negative and NULL are covered in NTILE partition_evaluator (

fn partition_evaluator(
) which called through create_udwf_window_expr which DF and also Comet calling.

But I'll add tests to assert it

UPD: double checked, those tests already in place and passing.

@comphead comphead requested a review from mbutrovich May 7, 2026 00:54
@comphead comphead enabled auto-merge May 7, 2026 14:55
@mbutrovich
Copy link
Copy Markdown
Contributor

Thanks @mbutrovich @coderfender non valid inputs, like 0, negative and NULL are covered in NTILE partition_evaluator (

fn partition_evaluator(

) which called through create_udwf_window_expr which DF and also Comet calling.
But I'll add tests to assert it

UPD: double checked, those tests already in place and passing.

So you're saying

            if n <= 0 {
                return exec_err!("NTILE requires a positive integer");
            }

is dead code then? We should remove it if it's not possible and replace with a debug_assert to make the invariant enforced in testing and invisible in release builds.

@comphead
Copy link
Copy Markdown
Contributor Author

comphead commented May 7, 2026

is dead code then? We should remove it if it's not possible and replace with a debug_assert to make the invariant enforced in testing and invisible in release builds.

The evaluator can be simplified, however it is the existing code, thanks for helping to make it cleaner. Addressed

Copy link
Copy Markdown
Contributor

@mbutrovich mbutrovich left a comment

Choose a reason for hiding this comment

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

Approved pending CI, thanks @comphead!

@comphead comphead added this pull request to the merge queue May 7, 2026
Merged via the queue into apache:main with commit 2bf1db5 May 7, 2026
35 checks passed
@comphead comphead deleted the ntile branch May 7, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

functions Changes to functions implementation sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NTILE returns wrong results

3 participants