This is an automated email from the ASF dual-hosted git repository.

dheres pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new b00723a2a0 Fix and Improve Sort Pushdown for Nested Loop and Hash Join 
(#12559)
b00723a2a0 is described below

commit b00723a2a0c0c1a5a30171729f7091407c819110
Author: Berkay Şahin <[email protected]>
AuthorDate: Mon Sep 23 16:31:01 2024 +0300

    Fix and Improve Sort Pushdown for Nested Loop and Hash Join (#12559)
    
    * Update sort_pushdown.rs
    
    * Update joins.slt
---
 .../core/src/physical_optimizer/sort_pushdown.rs   | 128 +++++++++++++++++++--
 datafusion/sqllogictest/test_files/joins.slt       |  37 ++++++
 2 files changed, 153 insertions(+), 12 deletions(-)

diff --git a/datafusion/core/src/physical_optimizer/sort_pushdown.rs 
b/datafusion/core/src/physical_optimizer/sort_pushdown.rs
index 41059ef7b1..34410bd6b1 100644
--- a/datafusion/core/src/physical_optimizer/sort_pushdown.rs
+++ b/datafusion/core/src/physical_optimizer/sort_pushdown.rs
@@ -22,22 +22,27 @@ use super::utils::{add_sort_above, is_sort};
 use crate::physical_optimizer::utils::{is_sort_preserving_merge, is_union, 
is_window};
 use crate::physical_plan::filter::FilterExec;
 use crate::physical_plan::joins::utils::calculate_join_output_ordering;
-use crate::physical_plan::joins::{HashJoinExec, SortMergeJoinExec};
+use crate::physical_plan::joins::SortMergeJoinExec;
 use crate::physical_plan::projection::ProjectionExec;
 use crate::physical_plan::repartition::RepartitionExec;
 use crate::physical_plan::sorts::sort::SortExec;
 use crate::physical_plan::tree_node::PlanContext;
 use crate::physical_plan::{ExecutionPlan, ExecutionPlanProperties};
 
-use datafusion_common::tree_node::{ConcreteTreeNode, Transformed, 
TreeNodeRecursion};
+use datafusion_common::tree_node::{
+    ConcreteTreeNode, Transformed, TreeNode, TreeNodeRecursion,
+};
 use datafusion_common::{plan_err, JoinSide, Result};
 use datafusion_expr::JoinType;
 use datafusion_physical_expr::expressions::Column;
+use datafusion_physical_expr::utils::collect_columns;
 use datafusion_physical_expr::{
     LexRequirementRef, PhysicalSortExpr, PhysicalSortRequirement,
 };
 use datafusion_physical_expr_common::sort_expr::LexRequirement;
 
+use hashbrown::HashSet;
+
 /// This is a "data class" we use within the [`EnforceSorting`] rule to push
 /// down [`SortExec`] in the plan. In some cases, we can reduce the total
 /// computational cost by pushing down `SortExec`s through some executors. The
@@ -253,7 +258,6 @@ fn pushdown_requirement_to_children(
         || plan.as_any().is::<FilterExec>()
         // TODO: Add support for Projection push down
         || plan.as_any().is::<ProjectionExec>()
-        || plan.as_any().is::<HashJoinExec>()
         || pushdown_would_violate_requirements(parent_required, plan.as_ref())
     {
         // If the current plan is a leaf node or can not maintain any of the 
input ordering, can not pushed down requirements.
@@ -277,15 +281,7 @@ fn pushdown_requirement_to_children(
             Ok(Some(vec![req]))
         }
     } else {
-        Ok(Some(
-            maintains_input_order
-                .into_iter()
-                .map(|flag| {
-                    (flag && !parent_required.is_empty())
-                        .then(|| parent_required.to_vec())
-                })
-                .collect(),
-        ))
+        handle_custom_pushdown(plan, parent_required, maintains_input_order)
     }
     // TODO: Add support for Projection push down
 }
@@ -483,6 +479,114 @@ fn shift_right_required(
     }
 }
 
+/// Handles the custom pushdown of parent-required sorting requirements down to
+/// the child execution plans, considering whether the input order is 
maintained.
+///
+/// # Arguments
+///
+/// * `plan` - A reference to an `ExecutionPlan` for which the pushdown will 
be applied.
+/// * `parent_required` - The sorting requirements expected by the parent node.
+/// * `maintains_input_order` - A vector of booleans indicating whether each 
child
+///   maintains the input order.
+///
+/// # Returns
+///
+/// Returns `Ok(Some(Vec<Option<LexRequirement>>))` if the sorting 
requirements can be
+/// pushed down, `Ok(None)` if not. On error, returns a `Result::Err`.
+fn handle_custom_pushdown(
+    plan: &Arc<dyn ExecutionPlan>,
+    parent_required: LexRequirementRef,
+    maintains_input_order: Vec<bool>,
+) -> Result<Option<Vec<Option<LexRequirement>>>> {
+    // If there's no requirement from the parent or the plan has no children, 
return early
+    if parent_required.is_empty() || plan.children().is_empty() {
+        return Ok(None);
+    }
+
+    // Collect all unique column indices used in the parent-required sorting 
expression
+    let all_indices: HashSet<usize> = parent_required
+        .iter()
+        .flat_map(|order| {
+            collect_columns(&order.expr)
+                .iter()
+                .map(|col| col.index())
+                .collect::<HashSet<_>>()
+        })
+        .collect();
+
+    // Get the number of fields in each child's schema
+    let len_of_child_schemas: Vec<usize> = plan
+        .children()
+        .iter()
+        .map(|c| c.schema().fields().len())
+        .collect();
+
+    // Find the index of the child that maintains input order
+    let Some(maintained_child_idx) = maintains_input_order
+        .iter()
+        .enumerate()
+        .find(|(_, m)| **m)
+        .map(|pair| pair.0)
+    else {
+        return Ok(None);
+    };
+
+    // Check if all required columns come from the child that maintains input 
order
+    let start_idx = len_of_child_schemas[..maintained_child_idx]
+        .iter()
+        .sum::<usize>();
+    let end_idx = start_idx + len_of_child_schemas[maintained_child_idx];
+    let all_from_maintained_child =
+        all_indices.iter().all(|i| i >= &start_idx && i < &end_idx);
+
+    // If all columns are from the maintained child, update the parent 
requirements
+    if all_from_maintained_child {
+        let sub_offset = len_of_child_schemas
+            .iter()
+            .take(maintained_child_idx)
+            .sum::<usize>();
+        // Transform the parent-required expression for the child schema by 
adjusting columns
+        let updated_parent_req = parent_required
+            .iter()
+            .map(|req| {
+                let child_schema = 
plan.children()[maintained_child_idx].schema();
+                let updated_columns = req
+                    .expr
+                    .clone()
+                    .transform_up(|expr| {
+                        if let Some(col) = 
expr.as_any().downcast_ref::<Column>() {
+                            let new_index = col.index() - sub_offset;
+                            Ok(Transformed::yes(Arc::new(Column::new(
+                                child_schema.field(new_index).name(),
+                                new_index,
+                            ))))
+                        } else {
+                            Ok(Transformed::no(expr))
+                        }
+                    })?
+                    .data;
+                Ok(PhysicalSortRequirement::new(updated_columns, req.options))
+            })
+            .collect::<Result<Vec<_>>>()?;
+
+        // Prepare the result, populating with the updated requirements for 
children that maintain order
+        let result = maintains_input_order
+            .iter()
+            .map(|&maintains_order| {
+                if maintains_order {
+                    Some(updated_parent_req.clone())
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        Ok(Some(result))
+    } else {
+        Ok(None)
+    }
+}
+
 /// Define the Requirements Compatibility
 #[derive(Debug)]
 enum RequirementsCompatibility {
diff --git a/datafusion/sqllogictest/test_files/joins.slt 
b/datafusion/sqllogictest/test_files/joins.slt
index 7d0262952b..64d5f6c7b8 100644
--- a/datafusion/sqllogictest/test_files/joins.slt
+++ b/datafusion/sqllogictest/test_files/joins.slt
@@ -4151,3 +4151,40 @@ DROP TABLE sales_global;
 
 statement ok
 DROP TABLE exchange_rates;
+
+# HashJoinExec and NestedLoopJoinExec can propagate SortExec down through its 
right child.
+
+statement ok
+CREATE TABLE left_table(a INT, b INT, c INT)
+
+statement ok
+CREATE TABLE right_table(x INT, y INT, z INT)
+
+query TT
+EXPLAIN SELECT * FROM left_table JOIN right_table ON 
left_table.a<right_table.x ORDER BY x;
+----
+logical_plan
+01)Sort: right_table.x ASC NULLS LAST
+02)--Inner Join:  Filter: left_table.a < right_table.x
+03)----TableScan: left_table projection=[a, b, c]
+04)----TableScan: right_table projection=[x, y, z]
+physical_plan
+01)NestedLoopJoinExec: join_type=Inner, filter=a@0 < x@1
+02)--MemoryExec: partitions=1, partition_sizes=[0]
+03)--SortExec: expr=[x@0 ASC NULLS LAST], preserve_partitioning=[false]
+04)----MemoryExec: partitions=1, partition_sizes=[0]
+
+query TT
+EXPLAIN SELECT * FROM left_table JOIN right_table ON 
left_table.a<right_table.x AND left_table.b=right_table.y ORDER BY x;
+----
+logical_plan
+01)Sort: right_table.x ASC NULLS LAST
+02)--Inner Join: left_table.b = right_table.y Filter: left_table.a < 
right_table.x
+03)----TableScan: left_table projection=[a, b, c]
+04)----TableScan: right_table projection=[x, y, z]
+physical_plan
+01)CoalesceBatchesExec: target_batch_size=3
+02)--HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(b@1, y@1)], 
filter=a@0 < x@1
+03)----MemoryExec: partitions=1, partition_sizes=[0]
+04)----SortExec: expr=[x@0 ASC NULLS LAST], preserve_partitioning=[false]
+05)------MemoryExec: partitions=1, partition_sizes=[0]
\ No newline at end of file


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

Reply via email to