Skip to content

Commit 5340653

Browse files
authored
fix: preserve range partitioning through joins (#23584)
## Which issue does this PR close? - Closes #23450. ## Rationale for this change #23184 allowed compatible range-partitioned inputs to satisfy partitioned inner joins without hash repartitioning. However, join output still downgraded `Partitioning::Range` to `UnknownPartitioning`, so downstream joins and aggregates could not reuse the preserved range layout and inserted avoidable hash repartitions. ## What changes are included in this PR? - Preserve `Partitioning::Range` in `adjust_right_output_partitioning`. - Shift right-side range-ordering column indexes into the joined schema while retaining split points and sort options. - Add unit coverage for compound range keys and non-default sort options. - Update SQL logic test plans for nested range joins and a range join feeding an aggregate. ## Are these changes tested? Yes. - `./dev/rust_lint.sh` - `cargo clippy --all-targets --all-features -- -D warnings` - `cargo test -p datafusion-physical-plan test_adjust_right_output_partitioning_preserves_range` - `cargo test --profile=ci --test sqllogictests -- range_partitioning.slt` - `ulimit -n 10240 && RUST_BACKTRACE=1 cargo test --profile ci --exclude datafusion-examples --exclude datafusion-benchmarks --exclude datafusion-cli --workspace --lib --tests --bins --features avro,json,backtrace,extended_tests,recursive_protection,parquet_encryption` ## Are there any user-facing changes? Yes. Physical plans can preserve proven range partitioning through joins, allowing compatible downstream joins and aggregates to avoid unnecessary hash repartitioning. There are no public API changes.
1 parent e123e6f commit 5340653

2 files changed

Lines changed: 76 additions & 24 deletions

File tree

datafusion/physical-plan/src/joins/utils.rs

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ use crate::metrics::{
3333
};
3434
use crate::projection::{ProjectionExec, ProjectionExpr};
3535
use crate::{
36-
ColumnStatistics, ExecutionPlan, ExecutionPlanProperties, Partitioning, Statistics,
36+
ColumnStatistics, ExecutionPlan, ExecutionPlanProperties, Partitioning,
37+
RangePartitioning, Statistics,
3738
};
3839
// compatibility
3940
pub use super::join_filter::JoinFilter;
@@ -68,7 +69,7 @@ use datafusion_common::stats::Precision;
6869
use datafusion_common::utils::normalize_float_zero;
6970
use datafusion_common::{
7071
DataFusionError, JoinSide, JoinType, NullEquality, Result, SharedResult,
71-
not_impl_err, plan_err,
72+
internal_datafusion_err, not_impl_err, plan_err,
7273
};
7374
use datafusion_expr::Operator;
7475
use datafusion_expr::interval_arithmetic::Interval;
@@ -146,9 +147,19 @@ pub fn adjust_right_output_partitioning(
146147
Partitioning::Hash(new_exprs, *size)
147148
}
148149
Partitioning::Range(range) => {
149-
// Range partitioning optimizer propagation is tracked in
150-
// https://github.com/apache/datafusion/issues/22395
151-
Partitioning::UnknownPartitioning(range.partition_count())
150+
let ordering = add_offset_to_physical_sort_exprs(
151+
range.ordering().iter().cloned(),
152+
left_columns_len as _,
153+
)?;
154+
let ordering = LexOrdering::new(ordering).ok_or_else(|| {
155+
internal_datafusion_err!(
156+
"Offsetting range partitioning produced an empty ordering"
157+
)
158+
})?;
159+
Partitioning::Range(RangePartitioning::new(
160+
ordering,
161+
range.split_points().to_vec(),
162+
))
152163
}
153164
result => result.clone(),
154165
};
@@ -2468,7 +2479,7 @@ mod tests {
24682479
use arrow::datatypes::{DataType, Fields};
24692480
use arrow::error::{ArrowError, Result as ArrowResult};
24702481
use datafusion_common::stats::Precision::{Absent, Exact, Inexact};
2471-
use datafusion_common::{ScalarValue, arrow_datafusion_err, arrow_err};
2482+
use datafusion_common::{ScalarValue, SplitPoint, arrow_datafusion_err, arrow_err};
24722483
use datafusion_physical_expr::PhysicalSortExpr;
24732484

24742485
use rstest::rstest;
@@ -4131,6 +4142,53 @@ mod tests {
41314142
assert_eq!(result.column_statistics[1].byte_size, Inexact(256));
41324143
}
41334144

4145+
#[test]
4146+
fn test_adjust_right_output_partitioning_preserves_range() -> Result<()> {
4147+
let split_points = vec![
4148+
SplitPoint::new(vec![
4149+
ScalarValue::Int32(Some(10)),
4150+
ScalarValue::Int32(Some(100)),
4151+
]),
4152+
SplitPoint::new(vec![
4153+
ScalarValue::Int32(Some(20)),
4154+
ScalarValue::Int32(Some(50)),
4155+
]),
4156+
];
4157+
let range = RangePartitioning::try_new(
4158+
LexOrdering::new([
4159+
PhysicalSortExpr::new(
4160+
Arc::new(Column::new("a", 0)),
4161+
SortOptions::new(false, true),
4162+
),
4163+
PhysicalSortExpr::new(
4164+
Arc::new(Column::new("b", 2)),
4165+
SortOptions::new(true, false),
4166+
),
4167+
])
4168+
.unwrap(),
4169+
split_points.clone(),
4170+
)?;
4171+
4172+
let adjusted = adjust_right_output_partitioning(&Partitioning::Range(range), 3)?;
4173+
let expected = Partitioning::Range(RangePartitioning::new(
4174+
LexOrdering::new([
4175+
PhysicalSortExpr::new(
4176+
Arc::new(Column::new("a", 3)),
4177+
SortOptions::new(false, true),
4178+
),
4179+
PhysicalSortExpr::new(
4180+
Arc::new(Column::new("b", 5)),
4181+
SortOptions::new(true, false),
4182+
),
4183+
])
4184+
.unwrap(),
4185+
split_points,
4186+
));
4187+
4188+
assert_eq!(adjusted, expected);
4189+
Ok(())
4190+
}
4191+
41344192
#[test]
41354193
fn test_calculate_join_output_ordering() -> Result<()> {
41364194
let left_ordering = LexOrdering::new(vec![

datafusion/sqllogictest/test_files/range_partitioning.slt

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,8 @@ set datafusion.optimizer.preserve_file_partitions = 0;
506506

507507
##########
508508
# TEST 15: Nested Range Joins
509-
# Compatible Range partitioning satisfies the lower join inputs. The upper join
510-
# still repairs the intermediate join output with Hash repartitioning because
511-
# HashJoinExec does not currently expose Range output partitioning.
509+
# Compatible Range partitioning is preserved through the lower join, allowing
510+
# the upper join to consume it without Hash repartitioning either input.
512511
##########
513512

514513
query TT
@@ -519,12 +518,10 @@ JOIN range_partitioned s ON r.range_key = s.range_key;
519518
----
520519
physical_plan
521520
01)HashJoinExec: mode=Partitioned, join_type=Inner, on=[(range_key@2, range_key@0)], projection=[range_key@0, value@1, value@3, value@5]
522-
02)--RepartitionExec: partitioning=Hash([range_key@2], 4), input_partitions=4
523-
03)----HashJoinExec: mode=Partitioned, join_type=Inner, on=[(range_key@0, range_key@0)]
524-
04)------DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
525-
05)------DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
526-
06)--RepartitionExec: partitioning=Hash([range_key@0], 4), input_partitions=4
527-
07)----DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
521+
02)--HashJoinExec: mode=Partitioned, join_type=Inner, on=[(range_key@0, range_key@0)]
522+
03)----DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
523+
04)----DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
524+
05)--DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
528525

529526
query IIII
530527
SELECT l.range_key, l.value, r.value, s.value
@@ -599,9 +596,8 @@ ORDER BY l.range_key;
599596

600597
##########
601598
# TEST 17: Range Join Feeds Aggregate
602-
# The join inputs avoid Hash repartitioning, but the aggregate above the join
603-
# still repartitions because HashJoinExec does not currently expose Range
604-
# output partitioning.
599+
# The join preserves compatible Range partitioning on range_key, allowing the
600+
# aggregate above it to avoid Hash repartitioning.
605601
##########
606602

607603
query TT
@@ -611,12 +607,10 @@ JOIN range_partitioned r ON l.range_key = r.range_key
611607
GROUP BY l.range_key;
612608
----
613609
physical_plan
614-
01)AggregateExec: mode=FinalPartitioned, gby=[range_key@0 as range_key], aggr=[sum(l.value + r.value)]
615-
02)--RepartitionExec: partitioning=Hash([range_key@0], 4), input_partitions=4
616-
03)----AggregateExec: mode=Partial, gby=[range_key@0 as range_key], aggr=[sum(l.value + r.value)]
617-
04)------HashJoinExec: mode=Partitioned, join_type=Inner, on=[(range_key@0, range_key@0)], projection=[range_key@0, value@1, value@3]
618-
05)--------DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
619-
06)--------DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
610+
01)AggregateExec: mode=SinglePartitioned, gby=[range_key@0 as range_key], aggr=[sum(l.value + r.value)]
611+
02)--HashJoinExec: mode=Partitioned, join_type=Inner, on=[(range_key@0, range_key@0)], projection=[range_key@0, value@1, value@3]
612+
03)----DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
613+
04)----DataSourceExec: file_groups=<slt:ignore>, projection=[range_key, value], output_partitioning=Range([range_key@0 ASC], [(10), (20), (30)], 4), file_type=csv, has_header=false
620614

621615
query II
622616
SELECT l.range_key, SUM(l.value + r.value)

0 commit comments

Comments
 (0)