Skip to content

Commit c9e4b7b

Browse files
ahmetenisAhmet Enis Erdoganmustafasrepo
authored
Partial Sort Plan Implementation (#9125)
* partial sort plan implementation * implement limiting the stream and remove topk * update documentation * rename replace_with_partial_sort tests * Fix imports * Add new end to end test * Address comments * cargo fmt * Address comments * addressing comments contd --------- Co-authored-by: Ahmet Enis Erdogan <[email protected]> Co-authored-by: Mustafa Akur <[email protected]>
1 parent 30d2be9 commit c9e4b7b

File tree

4 files changed

+1301
-1
lines changed

4 files changed

+1301
-1
lines changed

datafusion/core/src/physical_optimizer/enforce_sorting.rs

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ use datafusion_common::tree_node::{Transformed, TreeNode};
6363
use datafusion_common::{plan_err, DataFusionError};
6464
use datafusion_physical_expr::{PhysicalSortExpr, PhysicalSortRequirement};
6565
use datafusion_physical_plan::repartition::RepartitionExec;
66+
use datafusion_physical_plan::sorts::partial_sort::PartialSortExec;
67+
use datafusion_physical_plan::unbounded_output;
6668

6769
use itertools::izip;
6870

@@ -185,7 +187,10 @@ impl PhysicalOptimizerRule for EnforceSorting {
185187
let mut sort_pushdown = SortPushDown::new_default(updated_plan.plan);
186188
assign_initial_requirements(&mut sort_pushdown);
187189
let adjusted = sort_pushdown.transform_down(&pushdown_sorts)?;
188-
Ok(adjusted.plan)
190+
191+
adjusted
192+
.plan
193+
.transform_up(&|plan| Ok(Transformed::Yes(replace_with_partial_sort(plan)?)))
189194
}
190195

191196
fn name(&self) -> &str {
@@ -197,6 +202,42 @@ impl PhysicalOptimizerRule for EnforceSorting {
197202
}
198203
}
199204

205+
fn replace_with_partial_sort(
206+
plan: Arc<dyn ExecutionPlan>,
207+
) -> Result<Arc<dyn ExecutionPlan>> {
208+
let plan_any = plan.as_any();
209+
if let Some(sort_plan) = plan_any.downcast_ref::<SortExec>() {
210+
let child = sort_plan.children()[0].clone();
211+
if !unbounded_output(&child) {
212+
return Ok(plan);
213+
}
214+
215+
// here we're trying to find the common prefix for sorted columns that is required for the
216+
// sort and already satisfied by the given ordering
217+
let child_eq_properties = child.equivalence_properties();
218+
let sort_req = PhysicalSortRequirement::from_sort_exprs(sort_plan.expr());
219+
220+
let mut common_prefix_length = 0;
221+
while child_eq_properties
222+
.ordering_satisfy_requirement(&sort_req[0..common_prefix_length + 1])
223+
{
224+
common_prefix_length += 1;
225+
}
226+
if common_prefix_length > 0 {
227+
return Ok(Arc::new(
228+
PartialSortExec::new(
229+
sort_plan.expr().to_vec(),
230+
sort_plan.input().clone(),
231+
common_prefix_length,
232+
)
233+
.with_preserve_partitioning(sort_plan.preserve_partitioning())
234+
.with_fetch(sort_plan.fetch()),
235+
));
236+
}
237+
}
238+
Ok(plan)
239+
}
240+
200241
/// This function turns plans of the form
201242
/// ```text
202243
/// "SortExec: expr=\[a@0 ASC\]",
@@ -2205,4 +2246,101 @@ mod tests {
22052246

22062247
Ok(())
22072248
}
2249+
2250+
#[tokio::test]
2251+
async fn test_replace_with_partial_sort() -> Result<()> {
2252+
let schema = create_test_schema3()?;
2253+
let input_sort_exprs = vec![sort_expr("a", &schema)];
2254+
let unbounded_input = stream_exec_ordered(&schema, input_sort_exprs);
2255+
2256+
let physical_plan = sort_exec(
2257+
vec![sort_expr("a", &schema), sort_expr("c", &schema)],
2258+
unbounded_input,
2259+
);
2260+
2261+
let expected_input = [
2262+
"SortExec: expr=[a@0 ASC,c@2 ASC]",
2263+
" StreamingTableExec: partition_sizes=1, projection=[a, b, c, d, e], infinite_source=true, output_ordering=[a@0 ASC]"
2264+
];
2265+
let expected_optimized = [
2266+
"PartialSortExec: expr=[a@0 ASC,c@2 ASC], common_prefix_length=[1]",
2267+
" StreamingTableExec: partition_sizes=1, projection=[a, b, c, d, e], infinite_source=true, output_ordering=[a@0 ASC]",
2268+
];
2269+
assert_optimized!(expected_input, expected_optimized, physical_plan, true);
2270+
Ok(())
2271+
}
2272+
2273+
#[tokio::test]
2274+
async fn test_replace_with_partial_sort2() -> Result<()> {
2275+
let schema = create_test_schema3()?;
2276+
let input_sort_exprs = vec![sort_expr("a", &schema), sort_expr("c", &schema)];
2277+
let unbounded_input = stream_exec_ordered(&schema, input_sort_exprs);
2278+
2279+
let physical_plan = sort_exec(
2280+
vec![
2281+
sort_expr("a", &schema),
2282+
sort_expr("c", &schema),
2283+
sort_expr("d", &schema),
2284+
],
2285+
unbounded_input,
2286+
);
2287+
2288+
let expected_input = [
2289+
"SortExec: expr=[a@0 ASC,c@2 ASC,d@3 ASC]",
2290+
" StreamingTableExec: partition_sizes=1, projection=[a, b, c, d, e], infinite_source=true, output_ordering=[a@0 ASC, c@2 ASC]"
2291+
];
2292+
// let optimized
2293+
let expected_optimized = [
2294+
"PartialSortExec: expr=[a@0 ASC,c@2 ASC,d@3 ASC], common_prefix_length=[2]",
2295+
" StreamingTableExec: partition_sizes=1, projection=[a, b, c, d, e], infinite_source=true, output_ordering=[a@0 ASC, c@2 ASC]",
2296+
];
2297+
assert_optimized!(expected_input, expected_optimized, physical_plan, true);
2298+
Ok(())
2299+
}
2300+
2301+
#[tokio::test]
2302+
async fn test_not_replaced_with_partial_sort_for_bounded_input() -> Result<()> {
2303+
let schema = create_test_schema3()?;
2304+
let input_sort_exprs = vec![sort_expr("b", &schema), sort_expr("c", &schema)];
2305+
let parquet_input = parquet_exec_sorted(&schema, input_sort_exprs);
2306+
2307+
let physical_plan = sort_exec(
2308+
vec![
2309+
sort_expr("a", &schema),
2310+
sort_expr("b", &schema),
2311+
sort_expr("c", &schema),
2312+
],
2313+
parquet_input,
2314+
);
2315+
let expected_input = [
2316+
"SortExec: expr=[a@0 ASC,b@1 ASC,c@2 ASC]",
2317+
" ParquetExec: file_groups={1 group: [[x]]}, projection=[a, b, c, d, e], output_ordering=[b@1 ASC, c@2 ASC]"
2318+
];
2319+
let expected_no_change = expected_input;
2320+
assert_optimized!(expected_input, expected_no_change, physical_plan, false);
2321+
Ok(())
2322+
}
2323+
2324+
#[tokio::test]
2325+
async fn test_not_replaced_with_partial_sort_for_unbounded_input() -> Result<()> {
2326+
let schema = create_test_schema3()?;
2327+
let input_sort_exprs = vec![sort_expr("b", &schema), sort_expr("c", &schema)];
2328+
let unbounded_input = stream_exec_ordered(&schema, input_sort_exprs);
2329+
2330+
let physical_plan = sort_exec(
2331+
vec![
2332+
sort_expr("a", &schema),
2333+
sort_expr("b", &schema),
2334+
sort_expr("c", &schema),
2335+
],
2336+
unbounded_input,
2337+
);
2338+
let expected_input = [
2339+
"SortExec: expr=[a@0 ASC,b@1 ASC,c@2 ASC]",
2340+
" StreamingTableExec: partition_sizes=1, projection=[a, b, c, d, e], infinite_source=true, output_ordering=[b@1 ASC, c@2 ASC]"
2341+
];
2342+
let expected_no_change = expected_input;
2343+
assert_optimized!(expected_input, expected_no_change, physical_plan, true);
2344+
Ok(())
2345+
}
22082346
}

datafusion/physical-plan/src/sorts/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod builder;
2121
mod cursor;
2222
mod index;
2323
mod merge;
24+
pub mod partial_sort;
2425
pub mod sort;
2526
pub mod sort_preserving_merge;
2627
mod stream;

0 commit comments

Comments
 (0)