Skip to content

Conversation

@gene-bordegaray
Copy link
Contributor

@gene-bordegaray gene-bordegaray commented Nov 13, 2025

Which issue does this PR close?

Rationale for this change

There was an overly aggressive condition enforce_sorting rule was not handling UnionExec correctly. This conditions assumed that Unions did not maintain order causing SortExec nodes to be removed and then eventually added at a higher level, less efficiently.

What changes are included in this PR?

I removed this condition that now has changed the logic to properly take into account UnionExec's ability to maintain input ordering.

Are these changes tested?

Yes, previously failing tests were ignored and now are unignored and passing.

Are there any user-facing changes?

No

@github-actions github-actions bot added optimizer Optimizer rules core Core DataFusion crate labels Nov 13, 2025
@gene-bordegaray
Copy link
Contributor Author

Cc: @NGA-TRAN - leaving this in draft while I finish writing documentation on the enforce sorting rule, think this is the fix though 😄

Copy link
Contributor

@NGA-TRAN NGA-TRAN left a comment

Choose a reason for hiding this comment

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

LGTM

@rgehan : this will fix your request #18380
Can you help review it?

@gene-bordegaray : You may not need any long document explaining this unless you really want to do so. a short description is good enough

cc @alamb @2010YOUY01

AggregateExec: mode=Partial, gby=[id@0 as id], aggr=[], ordering_mode=Sorted
UnionExec
DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], output_ordering=[id@0 ASC NULLS LAST], file_type=parquet
SortExec: expr=[id@0 ASC NULLS LAST], preserve_partitioning=[false]
Copy link
Contributor

Choose a reason for hiding this comment

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

Great push down

SortExec: expr=[nullable_col@0 ASC], preserve_partitioning=[false]
DataSourceExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], file_type=parquet
SortExec: expr=[nullable_col@0 ASC], preserve_partitioning=[false]
DataSourceExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], file_type=parquet
Copy link
Contributor

@NGA-TRAN NGA-TRAN Nov 13, 2025

Choose a reason for hiding this comment

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

Can you explain why this is no longer needed? Because the input and output are now the same?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I was confused at first but this is what it means.

|| !plan.maintains_input_order()[idx]
|| is_union(plan)
{
} else if physical_ordering.is_none() || !plan.maintains_input_order()[idx] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wow. This is simple.
I wonder why this is_union is here in the first place 🤔

All the tests pass and @rgehan's reproducer now also pass means this is likely the right work

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I was very confused as well when stepping through the logic. The only thing I could think of is if it was thought that UnionExec was never able to maintain input order but it is implemented for the operator.

Interesting though since this is quoted from the documentation online about the operator: "UnionExec combines multiple inputs with the same schema by concatenating the partitions. It does not mix or copy data within or across partitions. Thus if the input partitions are sorted, the output partitions of the union are also sorted."

Copy link
Contributor

Choose a reason for hiding this comment

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

Very nice analysis

Copy link
Contributor

Choose a reason for hiding this comment

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

Wow. This is simple. I wonder why this is_union is here in the first place 🤔

I guess when implementing optimizer rules, it’s more common to be conservative than comprehensive if not sure, to avoid bugs.

@gene-bordegaray
Copy link
Contributor Author

LGTM

@rgehan : this will fix your request #18380 Can you help review it?

@gene-bordegaray : You may not need any long document explaining this unless you really want to do so. a short description is good enough

cc @alamb @2010YOUY01

Ok sounds good. More doing so for my own understanding and to give to others but shouldn't hold back this PR 😄

@gene-bordegaray gene-bordegaray marked this pull request as ready for review November 14, 2025 02:45
@alamb alamb changed the title Removed incorreect union check in enforce_sorting and updated tests Removed incorrect union check in enforce_sorting and updated tests Nov 14, 2025
Copy link
Contributor

@rgehan rgehan left a comment

Choose a reason for hiding this comment

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

LGTM, although I'm not knowledgeable enough to understand the impact of dropping this is_union() condition.

In addition to fixing the tests I previously added, I can confirm it also fixes my production use-cases 👍

Copy link
Contributor

@2010YOUY01 2010YOUY01 left a comment

Choose a reason for hiding this comment

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

Amazing work, thank you!

|| !plan.maintains_input_order()[idx]
|| is_union(plan)
{
} else if physical_ordering.is_none() || !plan.maintains_input_order()[idx] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wow. This is simple. I wonder why this is_union is here in the first place 🤔

I guess when implementing optimizer rules, it’s more common to be conservative than comprehensive if not sure, to avoid bugs.

Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

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

Thank you @gene-bordegaray @rgehan, and @NGA-TRAN

I reviewed this plan and code carefully and I agree there is no reason to force the sort after a union if the inputs are already sorted.

assert_snapshot!(
union_with_mix_of_presorted_and_explicitly_resorted_inputs_impl(false).await?,
@r#"
@r"
Copy link
Contributor

Choose a reason for hiding this comment

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

I reviewed this plan carefully and it looks good to me

@alamb alamb added this pull request to the merge queue Nov 20, 2025
@alamb
Copy link
Contributor

alamb commented Nov 20, 2025

Thanks again @gene-bordegaray

Merged via the queue into apache:main with commit 1833093 Nov 20, 2025
35 checks passed
@gene-bordegaray
Copy link
Contributor Author

Thanks again @gene-bordegaray

Glad to help 🫡

logan-keede pushed a commit to logan-keede/datafusion that referenced this pull request Nov 23, 2025
…pache#18661)

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

- Closes apache#18380.
- Closes apache#9898.

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

There was an overly aggressive condition enforce_sorting rule was not
handling UnionExec correctly. This conditions assumed that Unions did
not maintain order causing SortExec nodes to be removed and then
eventually added at a higher level, less efficiently.

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

I removed this condition that now has changed the logic to properly take
into account UnionExec's ability to maintain input ordering.

## Are these changes tested?

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

If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
-->

Yes, previously failing tests were ignored and now are unignored and
passing.

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

core Core DataFusion crate optimizer Optimizer rules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Preserving sort on UnionExec inputs instead of introducing a suboptimal top-level sort Teach UnionExec to require its inputs sorted

5 participants