-
Notifications
You must be signed in to change notification settings - Fork 2.3k
fix: keep a CoalescePartitionsExec required by a SinglePartition child #23948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cab7e9c
4b31a7d
c752ac4
8fb3d30
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -609,11 +609,49 @@ fn adjust_window_sort_removal( | |
| /// the plan, some of the remaining `RepartitionExec`s might become unnecessary. | ||
| /// Removes such `RepartitionExec`s from the plan as well. | ||
| fn remove_bottleneck_in_subplan( | ||
| requirements: PlanWithCorrespondingCoalescePartitions, | ||
| ) -> Result<PlanWithCorrespondingCoalescePartitions> { | ||
| // The root is the node `parallelize_sorts` is rewriting (a `SortExec`, | ||
| // `SortPreservingMergeExec` or `CoalescePartitionsExec`). Its own distribution | ||
| // requirement does not constrain the removal, because the caller drops the node and | ||
| // rebuilds the cascade around the result. | ||
| remove_bottleneck_in_subplan_impl(requirements, true) | ||
| } | ||
|
|
||
| fn remove_bottleneck_in_subplan_impl( | ||
| mut requirements: PlanWithCorrespondingCoalescePartitions, | ||
| is_root: bool, | ||
| ) -> Result<PlanWithCorrespondingCoalescePartitions> { | ||
| let plan = &requirements.plan; | ||
| // Below the root, a `CoalescePartitionsExec` feeding a child that requires | ||
| // `Distribution::SinglePartition` is not an avoidable bottleneck: it is what satisfies | ||
| // that requirement. Removing it leaves the parent with a multi-partition input it cannot | ||
| // accept, and nothing re-runs distribution enforcement afterwards, so the plan reaches | ||
| // `SanityCheckPlan` invalid. The traversal reaches such a node because | ||
| // `update_coalesce_ctx_children` marks a node as connected when *any* child qualifies: | ||
| // a `CollectLeft` `HashJoinExec` whose probe side is connected is descended into even | ||
| // though its build side must stay single-partition. | ||
| // | ||
| // Only `SinglePartition` is protected. A `HashPartitioned` child is in principle in the | ||
| // same position — a single-partition input trivially satisfies a hash requirement, so a | ||
| // coalesce below one is also load-bearing — but nothing puts a coalesce there: | ||
| // `ensure_distribution` satisfies a hash requirement with a `RepartitionExec`, never a | ||
| // `CoalescePartitionsExec`. Widening the check would be dead code today. | ||
| let dist_reqs = plan.input_distribution_requirements(); | ||
| let removable = |idx: usize| { | ||
| is_root | ||
| || !matches!( | ||
| dist_reqs.child_distribution(idx), | ||
| Some(Distribution::SinglePartition) | ||
| ) | ||
| }; | ||
|
Comment on lines
+641
to
+647
|
||
| let remove_from_first_child = requirements | ||
| .children | ||
| .first() | ||
| .is_some_and(|child| is_coalesce_partitions(&child.plan)) | ||
| && removable(0); | ||
| let children = &mut requirements.children; | ||
| if is_coalesce_partitions(&children[0].plan) { | ||
| if remove_from_first_child { | ||
| // We can safely use the 0th index since we have a `CoalescePartitionsExec`. | ||
| let mut new_child_node = children[0].children.swap_remove(0); | ||
| while new_child_node.plan.output_partitioning() == plan.output_partitioning() | ||
|
|
@@ -627,9 +665,14 @@ fn remove_bottleneck_in_subplan( | |
| requirements.children = requirements | ||
| .children | ||
| .into_iter() | ||
| .map(|node| { | ||
| if node.data { | ||
| remove_bottleneck_in_subplan(node) | ||
| .enumerate() | ||
| .map(|(idx, node)| { | ||
| // Deliberately conservative: not descending at all also skips legitimate | ||
| // cleanups *below* a protected child (a redundant second coalesce under the | ||
| // load-bearing one, say). This could later be narrowed to "descend, but | ||
| // protect only the topmost coalesce" if that turns out to matter. | ||
| if node.data && removable(idx) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth a short comment that this is deliberately conservative: skipping the descent entirely also skips legitimate cleanups below the protected child (e.g. a redundant second coalesce underneath the load-bearing one). Correctness-first is the right call for a fix — just flagging that the gate could later be narrowed to "descend, but protect only the top-level coalesce" if anyone cares about the missed optimization.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's the intended trade — the missed cleanup is a redundant coalesce under a load-bearing one, which I'd rather leave than risk narrowing wrong in a fix. Added a comment recording that, with "descend, but protect only the topmost coalesce" as the follow-up. |
||
| remove_bottleneck_in_subplan_impl(node, false) | ||
| } else { | ||
| Ok(node) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question (non-blocking): should this also protect
Distribution::HashPartitionedchildren? A single-partition input trivially satisfies a hash requirement, so a coalesce sitting under a hash-requiring child is load-bearing in the same way — removing it exposes whatever multi-partitioning is below, which may not be hashed on the right keys. I don't thinkEnsureRequirementsitself ever places a coalesce there (hash requirements get aRepartitionExec), so it's probably unreachable via this rule's own output — but a plan built by other rules or by hand could hit it. Fine to leave as-is if you agree it's unreachable; a one-line comment saying so would help the next reader.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed on both counts — same hazard in principle, but ensure_distribution satisfies a hash requirement with a RepartitionExec, never a coalesce, so widening the match would be dead code. Left as-is with a comment saying so.