Skip to content

feat: stage ORDER BY expression evaluation - #25358

Open
yashrb24 wants to merge 7 commits into
apache:mainfrom
yashrb24:staged-sort
Open

yashrb24 wants to merge 7 commits into
apache:mainfrom
yashrb24:staged-sort

Conversation

@yashrb24

@yashrb24 yashrb24 commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Related to #25033. This PR is mostly an ideation of the way we can bring in this change since it's kind of an invasive change to the sorting mechanics and thus wanted feedback.

Rationale for this change

Today, sorting evaluates all ORDER BY expressions before it compares rows. Some of this work is unnecessary when earlier keys already determine the order.

This change evaluates later expressions only for rows tied on earlier keys. This could help when later expressions are expensive and earlier keys resolve most rows. The drawback is that the extra sorting steps can outweigh these savings when expressions are cheap or many rows remain tied.

What changes are included in this POC PR?

  • Evaluate sort keys in configurable groups.
  • Sort by the first group, then evaluate each later group only for rows that remain tied.

Are there any user-facing changes?

had added these flags for my local testing, can remove if needed

  • datafusion.execution.enable_staged_sort: default false.
  • datafusion.execution.sort_key_group_size: default value is 5

Currently not extending to TopK or spill codepath

@github-actions github-actions Bot added documentation Improvements or additions to documentation sqllogictest SQL Logic Tests (.slt) common Related to common crate physical-plan Changes to the physical-plan crate labels Sep 16, 2026
@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion-common v55.1.0 (current)
       Built [  20.522s] (current)
     Parsing datafusion-common v55.1.0 (current)
      Parsed [   0.044s] (current)
    Building datafusion-common v55.1.0 (baseline)
       Built [  20.287s] (baseline)
     Parsing datafusion-common v55.1.0 (baseline)
      Parsed [   0.045s] (baseline)
    Checking datafusion-common v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.534s] 223 checks: 222 pass, 1 fail, 0 warn, 31 skip

--- failure constructible_struct_adds_field: struct exhaustively constructible through public API adds field ---

Description:
A pub struct that could be exhaustively constructed with a literal using only public API has a new pub field, breaking existing exhaustive literals.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field ExecutionOptions.enable_staged_sort in /home/runner/work/datafusion/datafusion/datafusion/common/src/config.rs:897
  field ExecutionOptions.sort_key_group_size in /home/runner/work/datafusion/datafusion/datafusion/common/src/config.rs:897

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  43.245s] datafusion-common
    Building datafusion-physical-plan v55.1.0 (current)
       Built [  21.625s] (current)
     Parsing datafusion-physical-plan v55.1.0 (current)
      Parsed [   0.124s] (current)
    Building datafusion-physical-plan v55.1.0 (baseline)
       Built [  21.754s] (baseline)
     Parsing datafusion-physical-plan v55.1.0 (baseline)
      Parsed [   0.127s] (baseline)
    Checking datafusion-physical-plan v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.451s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  44.932s] datafusion-physical-plan
    Building datafusion-sqllogictest v55.1.0 (current)
       Built [  57.872s] (current)
     Parsing datafusion-sqllogictest v55.1.0 (current)
      Parsed [   0.012s] (current)
    Building datafusion-sqllogictest v55.1.0 (baseline)
       Built [  62.449s] (baseline)
     Parsing datafusion-sqllogictest v55.1.0 (baseline)
      Parsed [   0.013s] (baseline)
    Checking datafusion-sqllogictest v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.074s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [ 122.324s] datafusion-sqllogictest

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Sep 16, 2026
@github-actions github-actions Bot removed documentation Improvements or additions to documentation sqllogictest SQL Logic Tests (.slt) labels Sep 16, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation sqllogictest SQL Logic Tests (.slt) labels Sep 16, 2026
@codecov-commenter

codecov-commenter commented Sep 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.52542% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.84%. Comparing base (22651d2) to head (094d0a2).
⚠️ Report is 384 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/physical-plan/src/sorts/sort.rs 91.44% 7 Missing and 28 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25358      +/-   ##
==========================================
+ Coverage   81.92%   82.84%   +0.91%     
==========================================
  Files        1135     1149      +14     
  Lines      427772   452993   +25221     
  Branches   427772   452993   +25221     
==========================================
+ Hits       350456   375281   +24825     
+ Misses      56373    54868    -1505     
- Partials    20943    22844    +1901     

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

@yashrb24
yashrb24 marked this pull request as ready for review September 18, 2026 04:38
@kosiew

kosiew commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

@yashrb24
Please amend the PR description to follow the .github/pull_request_template.md template.

@kosiew kosiew 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.

@yashrb24,

Thanks for working on this. I found two issues that I think need to be addressed before this is ready to merge.

}
arrow::compute::take_record_batch(batch, &UInt32Array::from(selection))?
};
let sort_columns = keys

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.

Staged sorting can skip later fallible or volatile ORDER BY expressions when earlier keys already resolve the order, so queries can behave differently depending on tie distribution. Please either document this experimental/error/volatile-evaluation contract and add tests that pin it, including that disabled staging stays eager, or preserve eager evaluation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

thank you. Though I wanted some directional feedback before actually having the full fledged implementation out and accounting for all cases. If the idea for this PR looks good I'll have these fixed too

}
}

fn staged_sort_batch_chunked(

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 new staged sorting path does not have focused behavioral tests. Please add coverage for staged versus eager equivalence, successive tie groups, null ordering, chunked output, the zero-tie/error case, and both single-batch and multiple in-memory-run paths.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

thanks for the catch, have added them

@kosiew kosiew 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.

@yashrb24,

Thanks for working on staged sorting. I think this is a promising direction, and the benchmarks show substantial benefits when earlier keys resolve most rows.

I found a correctness regression with column-free expressions in later stages. Please fix this and add regression coverage before merging.

The evaluation contract also needs to document that staged sorting can skip fallible or volatile expressions. Please clarify this behavior and its implications.

I also recommend adding reproducible benchmarks covering distinct keys, small and large tie groups, all-tied inputs, and multi-batch sorting. In a local micro-benchmark, 500k groups of two rows caused a 1.51x slowdown with a cheap CAST expression. Please investigate the per-group sorting and allocation overhead.

The optimization currently excludes TopK and does not avoid expression reevaluation during merging. It would help to clarify whether these are planned follow-ups.

Overall, I support continuing with the opt-in approach, but the correctness regression and evaluation-contract gap should be addressed before merging.

selection.extend_from_slice(&indices[range.clone()]);
}
arrow::compute::take_record_batch(
&batch.project(&stage.columns)?,

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.

When a later stage uses a column-free expression, stage.columns is empty and take_record_batch fails because the empty-column batch loses its row count. Could we preserve the selected row count using RecordBatchOptions::with_row_count(Some(selection.len())) or retain the full-batch gather for this case, and add tests with a zero-argument UDF and tied keys for both single and concatenated batches?

@kosiew kosiew 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.

@yashrb24

I think manual stage grouping does not seem like a practical production setting. The optimal group size depends on expression cost and tie distribution, which vary across queries and batches. With the default sort_key_group_size = 5, enabling staged sorting has no effect on typical two-key queries.

I suggest exploring three ways to automate this (all unvalidated and requiring benchmarks):

  1. Split by key cost: Group cheap keys together and start new stages before expensive expressions such as UDFs.
  2. Adapt at runtime: Use tie counts after each stage to decide whether to continue staging or evaluate the remaining keys together, avoiding overhead from many tiny tie groups.
  3. Simplify configuration: Keep at most an on/off switch and remove sort_key_group_size. This would also eliminate one of the two new public configuration fields contributing to the semver break.

I think automatic staging is a better long-term direction than requiring users to tune group sizes manually.

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

Labels

auto detected api change Auto detected API change common Related to common crate documentation Improvements or additions to documentation physical-plan Changes to the physical-plan crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants