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

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