-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Relax combine partial final rule #10913
Merged
alamb
merged 3 commits into
apache:main
from
synnada-ai:feature/relax_partial_final_combine
Jun 21, 2024
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,8 +27,7 @@ use crate::physical_plan::ExecutionPlan; | |
|
||
use datafusion_common::config::ConfigOptions; | ||
use datafusion_common::tree_node::{Transformed, TransformedResult, TreeNode}; | ||
use datafusion_physical_expr::expressions::Column; | ||
use datafusion_physical_expr::{AggregateExpr, PhysicalExpr}; | ||
use datafusion_physical_expr::{physical_exprs_equal, AggregateExpr, PhysicalExpr}; | ||
|
||
/// CombinePartialFinalAggregate optimizer rule combines the adjacent Partial and Final AggregateExecs | ||
/// into a Single AggregateExec if their grouping exprs and aggregate exprs equal. | ||
|
@@ -132,19 +131,23 @@ type GroupExprsRef<'a> = ( | |
&'a [Option<Arc<dyn PhysicalExpr>>], | ||
); | ||
|
||
type GroupExprs = ( | ||
PhysicalGroupBy, | ||
Vec<Arc<dyn AggregateExpr>>, | ||
Vec<Option<Arc<dyn PhysicalExpr>>>, | ||
); | ||
|
||
fn can_combine(final_agg: GroupExprsRef, partial_agg: GroupExprsRef) -> bool { | ||
let (final_group_by, final_aggr_expr, final_filter_expr) = | ||
normalize_group_exprs(final_agg); | ||
let (input_group_by, input_aggr_expr, input_filter_expr) = | ||
normalize_group_exprs(partial_agg); | ||
|
||
final_group_by.eq(&input_group_by) | ||
let (final_group_by, final_aggr_expr, final_filter_expr) = final_agg; | ||
let (input_group_by, input_aggr_expr, input_filter_expr) = partial_agg; | ||
|
||
// Compare output expressions of the partial, and input expressions of the final operator. | ||
physical_exprs_equal( | ||
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. 💯 |
||
&input_group_by.output_exprs(), | ||
&final_group_by.input_exprs(), | ||
) && input_group_by.groups() == final_group_by.groups() | ||
&& input_group_by.null_expr().len() == final_group_by.null_expr().len() | ||
&& input_group_by | ||
.null_expr() | ||
.iter() | ||
.zip(final_group_by.null_expr().iter()) | ||
.all(|((lhs_expr, lhs_str), (rhs_expr, rhs_str))| { | ||
lhs_expr.eq(rhs_expr) && lhs_str == rhs_str | ||
}) | ||
&& final_aggr_expr.len() == input_aggr_expr.len() | ||
&& final_aggr_expr | ||
.iter() | ||
|
@@ -160,41 +163,6 @@ fn can_combine(final_agg: GroupExprsRef, partial_agg: GroupExprsRef) -> bool { | |
) | ||
} | ||
|
||
// To compare the group expressions between the final and partial aggregations, need to discard all the column indexes and compare | ||
fn normalize_group_exprs(group_exprs: GroupExprsRef) -> GroupExprs { | ||
let (group, agg, filter) = group_exprs; | ||
let new_group_expr = group | ||
.expr() | ||
.iter() | ||
.map(|(expr, name)| (discard_column_index(expr.clone()), name.clone())) | ||
.collect::<Vec<_>>(); | ||
let new_group = PhysicalGroupBy::new( | ||
new_group_expr, | ||
group.null_expr().to_vec(), | ||
group.groups().to_vec(), | ||
); | ||
(new_group, agg.to_vec(), filter.to_vec()) | ||
} | ||
|
||
fn discard_column_index(group_expr: Arc<dyn PhysicalExpr>) -> Arc<dyn PhysicalExpr> { | ||
group_expr | ||
.clone() | ||
.transform(|expr| { | ||
let normalized_form: Option<Arc<dyn PhysicalExpr>> = | ||
match expr.as_any().downcast_ref::<Column>() { | ||
Some(column) => Some(Arc::new(Column::new(column.name(), 0))), | ||
None => None, | ||
}; | ||
Ok(if let Some(normalized_form) = normalized_form { | ||
Transformed::yes(normalized_form) | ||
} else { | ||
Transformed::no(expr) | ||
}) | ||
}) | ||
.data() | ||
.unwrap_or(group_expr) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1382,18 +1382,17 @@ physical_plan | |
02)--AggregateExec: mode=Final, gby=[], aggr=[COUNT(alias1)] | ||
03)----CoalescePartitionsExec | ||
04)------AggregateExec: mode=Partial, gby=[], aggr=[COUNT(alias1)] | ||
05)--------AggregateExec: mode=FinalPartitioned, gby=[alias1@0 as alias1], aggr=[] | ||
06)----------AggregateExec: mode=Partial, gby=[t1_id@0 as alias1], aggr=[] | ||
07)------------CoalesceBatchesExec: target_batch_size=2 | ||
08)--------------HashJoinExec: mode=Partitioned, join_type=Inner, on=[(t1_id@0, t2_id@0)], projection=[t1_id@0] | ||
09)----------------CoalesceBatchesExec: target_batch_size=2 | ||
10)------------------RepartitionExec: partitioning=Hash([t1_id@0], 2), input_partitions=2 | ||
11)--------------------RepartitionExec: partitioning=RoundRobinBatch(2), input_partitions=1 | ||
12)----------------------MemoryExec: partitions=1, partition_sizes=[1] | ||
13)----------------CoalesceBatchesExec: target_batch_size=2 | ||
14)------------------RepartitionExec: partitioning=Hash([t2_id@0], 2), input_partitions=2 | ||
15)--------------------RepartitionExec: partitioning=RoundRobinBatch(2), input_partitions=1 | ||
16)----------------------MemoryExec: partitions=1, partition_sizes=[1] | ||
05)--------AggregateExec: mode=SinglePartitioned, gby=[t1_id@0 as alias1], aggr=[] | ||
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. I agree this plan looks better and correct |
||
06)----------CoalesceBatchesExec: target_batch_size=2 | ||
07)------------HashJoinExec: mode=Partitioned, join_type=Inner, on=[(t1_id@0, t2_id@0)], projection=[t1_id@0] | ||
08)--------------CoalesceBatchesExec: target_batch_size=2 | ||
09)----------------RepartitionExec: partitioning=Hash([t1_id@0], 2), input_partitions=2 | ||
10)------------------RepartitionExec: partitioning=RoundRobinBatch(2), input_partitions=1 | ||
11)--------------------MemoryExec: partitions=1, partition_sizes=[1] | ||
12)--------------CoalesceBatchesExec: target_batch_size=2 | ||
13)----------------RepartitionExec: partitioning=Hash([t2_id@0], 2), input_partitions=2 | ||
14)------------------RepartitionExec: partitioning=RoundRobinBatch(2), input_partitions=1 | ||
15)--------------------MemoryExec: partitions=1, partition_sizes=[1] | ||
|
||
statement ok | ||
set datafusion.explain.logical_plan_only = true; | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I am not sure that just checking the length of the group bys is sufficient -- I think logically they must be the same.
It seems like the reason these weren't combined
Is becase of aliasing the exprs didn't match exactly --
t1_id@0 as alias1
didn't matchalias1@0 as alias1
even though I think they are logically equivalentSo for example, if we ever made the following plan (with actually different grouping expressions) after this change the code would incorrectly collapse them
However, I am not sure that such a plan would be valid 🤔
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.
We were trying to think whether it is possible for a valid plan to have a consecutive Partial/Final duo with differing GROUP BY expressions (unless of course it is manually generated that way w/o a query).
We weren't able to find an example of this and started to think it is not possible. That's why that check was deemed to inhibit better plans in some cases without adding any real protection.
We would appreciate some brain cycles from the community on this. If our suspicion is correct, this small PR will give us better plans in many cases.
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.
Thank you for the explanation.
My concern is that if someone ever did create a plan that didn't have the same grouping expression that this condition could apply and thus cause a very hard to debug failure.
I think we should at least add some comments to the check explaining the assumption (that a two phase grouping must have semantically the same grouping keys) to help future readers / developers. Then I think this PR is ok to merge.
I also do wonder if we have some pre-existing code to check two expressions for equality from different schemas by normalizing them or something, but I didn't try and check for it at the moment
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.
After some thinking, I realized that since we are checking expressions equality of the subsequent operators. Output group by expressions of the first operator and input group by expressions of the second operator must be equal. I re-introduced group by equality condition with this comparison. With this comparison, we still generate better plans without relaxing the check. It can be found in the commit