Skip to content

Commit 8ca2242

Browse files
authored
feat: derive SMJ sort options from left child during plan creation (#43)
1 parent 5e03e56 commit 8ca2242

1 file changed

Lines changed: 204 additions & 3 deletions

File tree

datafusion/core/src/physical_planner.rs

Lines changed: 204 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,15 +1473,29 @@ impl DefaultPhysicalPlanner {
14731473
&& session_state.config().repartition_joins()
14741474
&& !prefer_hash_join
14751475
{
1476-
// Use SortMergeJoin if hash join is not preferred
1477-
let join_on_len = join_on.len();
1476+
// Derive sort options from the left input's existing ordering
1477+
// rather than hardcoding SortOptions::default()
1478+
let sort_options: Vec<SortOptions> = join_on
1479+
.iter()
1480+
.map(|(left_col, _)| {
1481+
physical_left
1482+
.output_ordering()
1483+
.and_then(|ordering| {
1484+
ordering
1485+
.iter()
1486+
.find(|sort_expr| sort_expr.expr.eq(left_col))
1487+
.map(|sort_expr| sort_expr.options)
1488+
})
1489+
.unwrap_or_default()
1490+
})
1491+
.collect();
14781492
Arc::new(SortMergeJoinExec::try_new(
14791493
physical_left,
14801494
physical_right,
14811495
join_on,
14821496
join_filter,
14831497
*join_type,
1484-
vec![SortOptions::default(); join_on_len],
1498+
sort_options,
14851499
*null_equality,
14861500
)?)
14871501
} else if session_state.config().target_partitions() > 1
@@ -4356,4 +4370,191 @@ digraph {
43564370
assert_contains!(&err_str, "field nullability at index");
43574371
assert_contains!(&err_str, "field metadata at index");
43584372
}
4373+
4374+
/// Helper: make a session state configured to use SortMergeJoin
4375+
fn make_smj_session_state() -> SessionState {
4376+
let runtime = Arc::new(RuntimeEnv::default());
4377+
let config = SessionConfig::new()
4378+
.with_target_partitions(4)
4379+
.set_bool("datafusion.optimizer.prefer_hash_join", false)
4380+
.set_bool("datafusion.optimizer.skip_failed_rules", false);
4381+
SessionStateBuilder::new()
4382+
.with_config(config)
4383+
.with_runtime_env(runtime)
4384+
.with_default_features()
4385+
.build()
4386+
}
4387+
4388+
/// Helper: create a fully-optimized physical plan and return its
4389+
/// displayable string for snapshot testing
4390+
async fn plan_smj_display(ctx: &SessionContext, sql: &str) -> Result<String> {
4391+
let state = ctx.state();
4392+
let logical_plan = state.create_logical_plan(sql).await?;
4393+
let logical_plan = state.optimize(&logical_plan)?;
4394+
let planner = DefaultPhysicalPlanner::default();
4395+
let plan = planner.create_physical_plan(&logical_plan, &state).await?;
4396+
Ok(format!("{}", displayable(plan.as_ref()).indent(false)))
4397+
}
4398+
4399+
/// Test: left child sorted DESC on join key -> SMJ derives DESC sort
4400+
/// options, avoiding a re-sort on the left side
4401+
#[tokio::test]
4402+
async fn test_smj_sort_options_derived_from_left_desc() -> Result<()> {
4403+
let schema = Arc::new(Schema::new(vec![
4404+
Field::new("a", DataType::Int32, false),
4405+
Field::new("b", DataType::Int32, false),
4406+
]));
4407+
let batch = RecordBatch::try_new(
4408+
Arc::clone(&schema),
4409+
vec![
4410+
Arc::new(Int32Array::from(vec![3, 2, 1])),
4411+
Arc::new(Int32Array::from(vec![10, 20, 30])),
4412+
],
4413+
)?;
4414+
4415+
let left = MemTable::try_new(Arc::clone(&schema), vec![vec![batch.clone()]])?
4416+
.with_sort_order(vec![vec![
4417+
col("a").sort(false, false), // DESC, nulls last
4418+
]]);
4419+
let right = MemTable::try_new(Arc::clone(&schema), vec![vec![batch]])?;
4420+
4421+
let state = make_smj_session_state();
4422+
let ctx = SessionContext::new_with_state(state);
4423+
ctx.register_table("left_table", Arc::new(left))?;
4424+
ctx.register_table("right_table", Arc::new(right))?;
4425+
4426+
let plan = plan_smj_display(
4427+
&ctx,
4428+
"SELECT * FROM left_table JOIN right_table ON left_table.a = right_table.a",
4429+
)
4430+
.await?;
4431+
insta::assert_snapshot!(plan, @r"
4432+
SortMergeJoinExec: join_type=Inner, on=[(a@0, a@0)]
4433+
DataSourceExec: partitions=1, partition_sizes=[1], output_ordering=a@0 DESC NULLS LAST
4434+
SortExec: expr=[a@0 DESC NULLS LAST], preserve_partitioning=[false]
4435+
DataSourceExec: partitions=1, partition_sizes=[1]
4436+
");
4437+
Ok(())
4438+
}
4439+
4440+
/// Test: left child not sorted on join key -> SMJ falls back to default
4441+
/// ASC behavior
4442+
#[tokio::test]
4443+
async fn test_smj_sort_options_default_when_left_unsorted() -> Result<()> {
4444+
let schema = Arc::new(Schema::new(vec![
4445+
Field::new("a", DataType::Int32, false),
4446+
Field::new("b", DataType::Int32, false),
4447+
]));
4448+
let batch = RecordBatch::try_new(
4449+
Arc::clone(&schema),
4450+
vec![
4451+
Arc::new(Int32Array::from(vec![1, 2, 3])),
4452+
Arc::new(Int32Array::from(vec![10, 20, 30])),
4453+
],
4454+
)?;
4455+
4456+
let left = MemTable::try_new(Arc::clone(&schema), vec![vec![batch.clone()]])?;
4457+
let right = MemTable::try_new(Arc::clone(&schema), vec![vec![batch]])?;
4458+
4459+
let state = make_smj_session_state();
4460+
let ctx = SessionContext::new_with_state(state);
4461+
ctx.register_table("left_table", Arc::new(left))?;
4462+
ctx.register_table("right_table", Arc::new(right))?;
4463+
4464+
let plan = plan_smj_display(
4465+
&ctx,
4466+
"SELECT * FROM left_table JOIN right_table ON left_table.a = right_table.a",
4467+
)
4468+
.await?;
4469+
insta::assert_snapshot!(plan, @r"
4470+
SortMergeJoinExec: join_type=Inner, on=[(a@0, a@0)]
4471+
SortExec: expr=[a@0 ASC], preserve_partitioning=[false]
4472+
DataSourceExec: partitions=1, partition_sizes=[1]
4473+
SortExec: expr=[a@0 ASC], preserve_partitioning=[false]
4474+
DataSourceExec: partitions=1, partition_sizes=[1]
4475+
");
4476+
Ok(())
4477+
}
4478+
4479+
/// Test: multi-column join where left child's ordering partially matches
4480+
#[tokio::test]
4481+
async fn test_smj_sort_options_partial_match_multi_column() -> Result<()> {
4482+
let schema = Arc::new(Schema::new(vec![
4483+
Field::new("a", DataType::Int32, false),
4484+
Field::new("b", DataType::Int32, false),
4485+
]));
4486+
let batch = RecordBatch::try_new(
4487+
Arc::clone(&schema),
4488+
vec![
4489+
Arc::new(Int32Array::from(vec![1, 2, 3])),
4490+
Arc::new(Int32Array::from(vec![30, 20, 10])),
4491+
],
4492+
)?;
4493+
4494+
// Left sorted by "a" DESC only; "b" is not in the ordering
4495+
let left = MemTable::try_new(Arc::clone(&schema), vec![vec![batch.clone()]])?
4496+
.with_sort_order(vec![vec![
4497+
col("a").sort(false, false), // DESC, nulls last
4498+
]]);
4499+
let right = MemTable::try_new(Arc::clone(&schema), vec![vec![batch]])?;
4500+
4501+
let state = make_smj_session_state();
4502+
let ctx = SessionContext::new_with_state(state);
4503+
ctx.register_table("left_table", Arc::new(left))?;
4504+
ctx.register_table("right_table", Arc::new(right))?;
4505+
4506+
let plan = plan_smj_display(
4507+
&ctx,
4508+
"SELECT * FROM left_table JOIN right_table \
4509+
ON left_table.a = right_table.a AND left_table.b = right_table.b",
4510+
)
4511+
.await?;
4512+
insta::assert_snapshot!(plan, @r"
4513+
SortMergeJoinExec: join_type=Inner, on=[(a@0, a@0), (b@1, b@1)]
4514+
SortExec: expr=[a@0 DESC NULLS LAST, b@1 ASC], preserve_partitioning=[false]
4515+
DataSourceExec: partitions=1, partition_sizes=[1], output_ordering=a@0 DESC NULLS LAST
4516+
SortExec: expr=[a@0 DESC NULLS LAST, b@1 ASC], preserve_partitioning=[false]
4517+
DataSourceExec: partitions=1, partition_sizes=[1]
4518+
");
4519+
Ok(())
4520+
}
4521+
4522+
/// Test: both sides already sorted DESC on join key -> no SortExec needed
4523+
#[tokio::test]
4524+
async fn test_smj_sort_options_both_sides_sorted_desc() -> Result<()> {
4525+
let schema = Arc::new(Schema::new(vec![
4526+
Field::new("a", DataType::Int32, false),
4527+
Field::new("b", DataType::Int32, false),
4528+
]));
4529+
let batch = RecordBatch::try_new(
4530+
Arc::clone(&schema),
4531+
vec![
4532+
Arc::new(Int32Array::from(vec![3, 2, 1])),
4533+
Arc::new(Int32Array::from(vec![10, 20, 30])),
4534+
],
4535+
)?;
4536+
4537+
let sort_order = vec![vec![col("a").sort(false, false)]]; // DESC, nulls last
4538+
let left = MemTable::try_new(Arc::clone(&schema), vec![vec![batch.clone()]])?
4539+
.with_sort_order(sort_order.clone());
4540+
let right = MemTable::try_new(Arc::clone(&schema), vec![vec![batch]])?
4541+
.with_sort_order(sort_order);
4542+
4543+
let state = make_smj_session_state();
4544+
let ctx = SessionContext::new_with_state(state);
4545+
ctx.register_table("left_table", Arc::new(left))?;
4546+
ctx.register_table("right_table", Arc::new(right))?;
4547+
4548+
let plan = plan_smj_display(
4549+
&ctx,
4550+
"SELECT * FROM left_table JOIN right_table ON left_table.a = right_table.a",
4551+
)
4552+
.await?;
4553+
insta::assert_snapshot!(plan, @r"
4554+
SortMergeJoinExec: join_type=Inner, on=[(a@0, a@0)]
4555+
DataSourceExec: partitions=1, partition_sizes=[1], output_ordering=a@0 DESC NULLS LAST
4556+
DataSourceExec: partitions=1, partition_sizes=[1], output_ordering=a@0 DESC NULLS LAST
4557+
");
4558+
Ok(())
4559+
}
43594560
}

0 commit comments

Comments
 (0)