sunchao commented on code in PR #25589:
URL: https://github.com/apache/datafusion/pull/25589#discussion_r4209329677


##########
datafusion/physical-optimizer/src/ensure_requirements/enforce_distribution.rs:
##########
@@ -554,6 +554,123 @@ fn reorder_current_join_keys(
     }
 }
 
+/// Finds the permutation that aligns this input's join keys with its range 
keys.
+fn range_join_key_positions(
+    input: &dyn ExecutionPlan,
+    join_keys: &[PhysicalExprRef],
+) -> Option<Vec<usize>> {
+    let Partitioning::Range(range) = input.output_partitioning() else {
+        return None;
+    };
+
+    if join_keys.len() != range.ordering().len() {
+        return None;
+    }
+
+    let expected = range
+        .ordering()
+        .iter()
+        .map(|sort_expr| Arc::clone(&sort_expr.expr))
+        .collect::<Vec<_>>();
+
+    expected_expr_positions(join_keys, &expected).or_else(|| {
+        let eq_group = input.equivalence_properties().eq_group();
+        if eq_group.is_empty() {
+            return None;
+        }
+
+        let normalized_keys = join_keys
+            .iter()
+            .map(|expr| eq_group.normalize_expr(Arc::clone(expr)))
+            .collect::<Vec<_>>();
+        let normalized_expected = expected
+            .iter()
+            .map(|expr| eq_group.normalize_expr(Arc::clone(expr)))
+            .collect::<Vec<_>>();
+
+        expected_expr_positions(&normalized_keys, &normalized_expected)
+    })
+}
+
+/// Returns the non-identity permutation that aligns `on` with a
+/// range-partitioned input, trying the left input first and then the right.
+fn range_aligned_join_key_positions(
+    left: &Arc<dyn ExecutionPlan>,
+    right: &Arc<dyn ExecutionPlan>,
+    on: &[(PhysicalExprRef, PhysicalExprRef)],
+) -> Option<Vec<usize>> {
+    if !matches!(left.output_partitioning(), Partitioning::Range(_))
+        && !matches!(right.output_partitioning(), Partitioning::Range(_))
+    {
+        return None;
+    }
+
+    let keys = extract_join_keys(on);
+    let positions = range_join_key_positions(left.as_ref(), &keys.left_keys)
+        .or_else(|| range_join_key_positions(right.as_ref(), 
&keys.right_keys))?;
+
+    if positions.iter().copied().eq(0..positions.len()) {
+        return None;
+    }
+
+    Some(positions)
+}
+
+/// Aligns a partitioned join's equi-key pairs with an existing input range
+/// ordering, so compatible Range inputs satisfy the join's co-partitioning
+/// requirement without a repartition. Covers partitioned hash joins and
+/// sort-merge joins. The permutation moves whole key pairs and, for
+/// sort-merge joins, the matching `sort_options`.
+fn reorder_join_keys_to_range_inputs(
+    plan: Arc<dyn ExecutionPlan>,
+) -> Result<Arc<dyn ExecutionPlan>> {
+    if let Some(join) = plan.downcast_ref::<HashJoinExec>() {
+        if join.mode != PartitionMode::Partitioned {
+            return Ok(plan);
+        }
+        let Some(positions) =
+            range_aligned_join_key_positions(&join.left, &join.right, &join.on)
+        else {
+            return Ok(plan);
+        };
+        let new_on = positions
+            .into_iter()
+            .map(|index| join.on[index].clone())
+            .collect();
+        return join.builder().with_on(new_on).build_exec();
+    }
+
+    if let Some(join) = plan.downcast_ref::<SortMergeJoinExec>() {
+        let Some(positions) =
+            range_aligned_join_key_positions(&join.left, &join.right, &join.on)

Review Comment:
   Thanks for the fix. I reran the original reproducer on `3c53a04`, and that 
case now passes. There is still a **[P2] streaming regression when the ordering 
check relies on a per-partition constant**.
   
   In `forces_unbounded_sort` ([lines 
609–611](https://github.com/apache/datafusion/blob/3c53a047ef4c6b0e3fa97102f97e4b33f0d7c830/datafusion/physical-optimizer/src/ensure_requirements/enforce_distribution.rs#L609-L611)),
 the check uses the child's properties before distribution enforcement. If `a` 
is a `ConstExpr` with `AcrossPartitions::Heterogeneous`, an input explicitly 
ordered by `(b, a)` satisfies the proposed `(a, b)` locally. However, the 
subsequent multi-input `RepartitionExec` calls 
`clear_per_partition_constants()`. Its output properties no longer prove the 
rewritten ordering, so sorting enforcement inserts a full `SortExec` on the 
unbounded input.
   
   I reproduced this with a bounded left input using two `Range(a, b)` 
partitions split at `(10, 0)`, and an unbounded right input using 
`UnknownPartitioning(2)`. Both advertise ordering `(b, a)` and the valid 
per-partition constant `a`. Partition `p = 0, 1` contains 16 rows `a = 2 + 
10*p, b = 2*i + p` for `i = 0..15`; the right streams emit their batch and then 
remain pending. With target partitions set to 2 and round-robin/file-scan 
repartitioning disabled:
   
   - On merge base `6047791`, sanity validation passes and the join emits a 
first batch.
   - On head `3c53a04`, both `top_down_join_key_reordering` settings insert the 
blocking sort: `SanityCheckPlan` rejects the plan with `Cannot execute pipeline 
breaking queries`, and direct execution times out waiting one second for its 
first batch.
   - The same rows with the left advertised as `UnknownPartitioning(2)` stream 
successfully on both revisions. Finite controls return all 32 exact expected 
rows on both revisions. The six-case diagnostic passes 6/6 on base and 4/6 on 
head.
   
   Could the guard use ordering guarantees that survive the planned exchanges? 
A conservative option is to clear per-partition constants on a cloned 
equivalence-properties value before checking a multi-partition input. Please 
include a regression covering this case before resolving the streaming thread.



##########
datafusion/physical-optimizer/src/ensure_requirements/enforce_distribution.rs:
##########
@@ -554,6 +554,155 @@ fn reorder_current_join_keys(
     }
 }
 
+/// Finds the permutation that aligns this input's join keys with its range 
keys.
+fn range_join_key_positions(
+    input: &dyn ExecutionPlan,
+    join_keys: &[PhysicalExprRef],
+) -> Option<Vec<usize>> {
+    let Partitioning::Range(range) = input.output_partitioning() else {
+        return None;
+    };
+
+    if join_keys.len() != range.ordering().len() {

Review Comment:
   Confirmed on `3c53a04`: this has a concrete cost for sort-merge joins. With 
`target_partitions = 1`, two bounded inputs using `Range([a, b], [])`, and both 
inputs already ordered by `(b, a)`, the original `(b, a)` join is valid and 
needs no exchange or sort. Alignment changes the keys to `(a, b)` and adds 
**two full `SortExec` operators**, while still adding no exchange. I reproduced 
this under both `top_down_join_key_reordering` settings.
   
   Please skip alignment when the inputs will remain single-partition and there 
is no distribution benefit. This is plan evidence of unnecessary sorting; I did 
not measure runtime. A regression with `target_partitions = 1` would make the 
intended behavior explicit.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to