Skip to content

Commit f81de62

Browse files
allow range partitioning on symmetric and sort merge joins
1 parent 096012e commit f81de62

7 files changed

Lines changed: 344 additions & 33 deletions

File tree

datafusion/catalog/src/streaming.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ use async_trait::async_trait;
2424
use datafusion_common::{DFSchema, Result, plan_err};
2525
use datafusion_expr::{Expr, SortExpr, TableType};
2626
use datafusion_physical_expr::equivalence::project_ordering;
27-
use datafusion_physical_expr::{LexOrdering, create_physical_sort_exprs};
27+
use datafusion_physical_expr::projection::ProjectionMapping;
28+
use datafusion_physical_expr::{
29+
EquivalenceProperties, LexOrdering, Partitioning, create_physical_sort_exprs,
30+
};
2831
use datafusion_physical_plan::ExecutionPlan;
2932
use datafusion_physical_plan::streaming::{PartitionStream, StreamingTableExec};
3033
use log::debug;
@@ -38,6 +41,7 @@ pub struct StreamingTable {
3841
partitions: Vec<Arc<dyn PartitionStream>>,
3942
infinite: bool,
4043
sort_order: Vec<SortExpr>,
44+
output_partitioning: Option<Partitioning>,
4145
}
4246

4347
impl StreamingTable {
@@ -62,6 +66,7 @@ impl StreamingTable {
6266
partitions,
6367
infinite: false,
6468
sort_order: vec![],
69+
output_partitioning: None,
6570
})
6671
}
6772

@@ -76,6 +81,33 @@ impl StreamingTable {
7681
self.sort_order = sort_order;
7782
self
7883
}
84+
85+
/// Declares the output partitioning of this streaming table.
86+
///
87+
/// The partitioning expressions refer to the table schema before scan
88+
/// projection. If a scan projection removes a partitioning expression, the
89+
/// physical plan reports unknown partitioning.
90+
pub fn with_output_partitioning(mut self, output_partitioning: Partitioning) -> Self {
91+
self.output_partitioning = Some(output_partitioning);
92+
self
93+
}
94+
95+
fn output_partitioning(
96+
&self,
97+
projection: Option<&Vec<usize>>,
98+
) -> Result<Partitioning> {
99+
let Some(output_partitioning) = &self.output_partitioning else {
100+
return Ok(Partitioning::UnknownPartitioning(self.partitions.len()));
101+
};
102+
let Some(projection) = projection else {
103+
return Ok(output_partitioning.clone());
104+
};
105+
106+
let projection_mapping =
107+
ProjectionMapping::from_indices(projection, &self.schema)?;
108+
let eq_properties = EquivalenceProperties::new(Arc::clone(&self.schema));
109+
Ok(output_partitioning.project(&projection_mapping, &eq_properties))
110+
}
79111
}
80112

81113
#[async_trait]
@@ -119,13 +151,16 @@ impl TableProvider for StreamingTable {
119151
vec![]
120152
};
121153

122-
Ok(Arc::new(StreamingTableExec::try_new(
154+
let exec = StreamingTableExec::try_new(
123155
Arc::clone(&self.schema),
124156
self.partitions.clone(),
125157
projection,
126158
LexOrdering::new(physical_sort),
127159
self.infinite,
128160
limit,
129-
)?))
161+
)?
162+
.with_output_partitioning(self.output_partitioning(projection)?)?;
163+
164+
Ok(Arc::new(exec))
130165
}
131166
}

datafusion/core/tests/physical_optimizer/sanity_checker.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
3030
use datafusion::datasource::stream::{FileStreamProvider, StreamConfig, StreamTable};
3131
use datafusion::prelude::{CsvReadOptions, SessionContext};
3232
use datafusion_common::config::ConfigOptions;
33-
use datafusion_common::{JoinType, Result, ScalarValue};
33+
use datafusion_common::{JoinType, NullEquality, Result, ScalarValue};
3434
use datafusion_physical_expr::expressions::{Literal, col};
3535
use datafusion_physical_expr::{Partitioning, RangePartitioning, SplitPoint};
3636
use datafusion_physical_expr_common::sort_expr::LexOrdering;
3737
use datafusion_physical_optimizer::PhysicalOptimizerRule;
3838
use datafusion_physical_optimizer::sanity_checker::SanityCheckPlan;
39+
use datafusion_physical_plan::joins::{StreamJoinPartitionMode, SymmetricHashJoinExec};
3940
use datafusion_physical_plan::repartition::RepartitionExec;
4041
use datafusion_physical_plan::{ExecutionPlan, displayable};
4142

@@ -444,6 +445,77 @@ fn test_partitioned_hash_join_requires_co_partitioned_children() -> Result<()> {
444445
Ok(())
445446
}
446447

448+
#[test]
449+
fn test_sort_merge_join_requires_co_partitioned_children() -> Result<()> {
450+
let schema = create_test_schema2();
451+
let join_on = vec![(col("a", &schema)?, col("a", &schema)?)];
452+
let ordering: LexOrdering = [sort_expr("a", &schema)].into();
453+
454+
let compatible_join = sort_merge_join_exec(
455+
sort_exec_with_preserve_partitioning(
456+
ordering.clone(),
457+
range_partitioned_exec(&schema, "a", [10])?,
458+
),
459+
sort_exec_with_preserve_partitioning(
460+
ordering.clone(),
461+
range_partitioned_exec(&schema, "a", [10])?,
462+
),
463+
&join_on,
464+
&JoinType::Inner,
465+
);
466+
assert_sanity_check(&compatible_join, true);
467+
468+
let incompatible_join = sort_merge_join_exec(
469+
sort_exec_with_preserve_partitioning(
470+
ordering.clone(),
471+
range_partitioned_exec(&schema, "a", [10])?,
472+
),
473+
sort_exec_with_preserve_partitioning(
474+
ordering,
475+
range_partitioned_exec(&schema, "a", [20])?,
476+
),
477+
&join_on,
478+
&JoinType::Inner,
479+
);
480+
assert_sanity_check(&incompatible_join, false);
481+
482+
Ok(())
483+
}
484+
485+
#[test]
486+
fn test_symmetric_hash_join_requires_co_partitioned_children() -> Result<()> {
487+
let schema = create_test_schema2();
488+
let join_on = vec![(col("a", &schema)?, col("a", &schema)?)];
489+
490+
let compatible_join = Arc::new(SymmetricHashJoinExec::try_new(
491+
range_partitioned_exec(&schema, "a", [10])?,
492+
range_partitioned_exec(&schema, "a", [10])?,
493+
join_on.clone(),
494+
None,
495+
&JoinType::Inner,
496+
NullEquality::NullEqualsNothing,
497+
None,
498+
None,
499+
StreamJoinPartitionMode::Partitioned,
500+
)?) as Arc<dyn ExecutionPlan>;
501+
assert_sanity_check(&compatible_join, true);
502+
503+
let incompatible_join = Arc::new(SymmetricHashJoinExec::try_new(
504+
range_partitioned_exec(&schema, "a", [10])?,
505+
range_partitioned_exec(&schema, "a", [20])?,
506+
join_on,
507+
None,
508+
&JoinType::Inner,
509+
NullEquality::NullEqualsNothing,
510+
None,
511+
None,
512+
StreamJoinPartitionMode::Partitioned,
513+
)?) as Arc<dyn ExecutionPlan>;
514+
assert_sanity_check(&incompatible_join, false);
515+
516+
Ok(())
517+
}
518+
447519
#[tokio::test]
448520
/// Tests that plan is valid when the sort requirements are satisfied.
449521
async fn test_bounded_window_agg_sort_requirement() -> Result<()> {

datafusion/physical-plan/src/joins/sort_merge_join/exec.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ use crate::spill::spill_manager::SpillManager;
4141
use crate::statistics::{ChildStats, StatisticsArgs};
4242
use crate::{
4343
DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, ExecutionPlanProperties,
44-
PlanProperties, SendableRecordBatchStream, Statistics, check_if_same_properties,
44+
InputDistributionRequirements, PlanProperties, SendableRecordBatchStream, Statistics,
45+
check_if_same_properties,
4546
};
4647

4748
use arrow::compute::SortOptions;
@@ -413,16 +414,17 @@ impl ExecutionPlan for SortMergeJoinExec {
413414
self.input_distribution_requirements().into_per_child()
414415
}
415416

416-
fn input_distribution_requirements(&self) -> crate::InputDistributionRequirements {
417+
fn input_distribution_requirements(&self) -> InputDistributionRequirements {
417418
let (left_expr, right_expr) = self
418419
.on
419420
.iter()
420421
.map(|(l, r)| (Arc::clone(l), Arc::clone(r)))
421422
.unzip();
422-
crate::InputDistributionRequirements::new(vec![
423+
InputDistributionRequirements::co_partitioned(vec![
423424
Distribution::KeyPartitioned(left_expr),
424425
Distribution::KeyPartitioned(right_expr),
425426
])
427+
.allow_range_satisfaction_for_key_partitioning()
426428
}
427429

428430
fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ use crate::projection::{
5353
use crate::stream::EmptyRecordBatchStream;
5454
use crate::{
5555
DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, ExecutionPlanProperties,
56-
PlanProperties, RecordBatchStream, SendableRecordBatchStream,
56+
InputDistributionRequirements, PlanProperties, RecordBatchStream,
57+
SendableRecordBatchStream,
5758
joins::StreamJoinPartitionMode,
5859
metrics::{ExecutionPlanMetricsSet, MetricsSet},
5960
};
@@ -415,23 +416,27 @@ impl ExecutionPlan for SymmetricHashJoinExec {
415416
self.input_distribution_requirements().into_per_child()
416417
}
417418

418-
fn input_distribution_requirements(&self) -> crate::InputDistributionRequirements {
419-
crate::InputDistributionRequirements::new(match self.mode {
419+
fn input_distribution_requirements(&self) -> InputDistributionRequirements {
420+
match self.mode {
420421
StreamJoinPartitionMode::Partitioned => {
421422
let (left_expr, right_expr) = self
422423
.on
423424
.iter()
424425
.map(|(l, r)| (Arc::clone(l) as _, Arc::clone(r) as _))
425426
.unzip();
426-
vec![
427+
InputDistributionRequirements::co_partitioned(vec![
427428
Distribution::KeyPartitioned(left_expr),
428429
Distribution::KeyPartitioned(right_expr),
429-
]
430+
])
431+
.allow_range_satisfaction_for_key_partitioning()
430432
}
431433
StreamJoinPartitionMode::SinglePartition => {
432-
vec![Distribution::SinglePartition, Distribution::SinglePartition]
434+
InputDistributionRequirements::new(vec![
435+
Distribution::SinglePartition,
436+
Distribution::SinglePartition,
437+
])
433438
}
434-
})
439+
}
435440
}
436441

437442
fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {

datafusion/physical-plan/src/streaming.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use crate::{ExecutionPlan, Partitioning, SendableRecordBatchStream};
3535
use arrow::datatypes::{Schema, SchemaRef};
3636
use datafusion_common::{Result, internal_err, plan_err};
3737
use datafusion_execution::TaskContext;
38+
use datafusion_physical_expr::projection::ProjectionMapping;
3839
use datafusion_physical_expr::{EquivalenceProperties, LexOrdering};
3940

4041
use async_trait::async_trait;
@@ -100,7 +101,7 @@ impl StreamingTableExec {
100101
let cache = Self::compute_properties(
101102
Arc::clone(&projected_schema),
102103
projected_output_ordering.clone(),
103-
&partitions,
104+
Partitioning::UnknownPartitioning(partitions.len()),
104105
infinite,
105106
);
106107
Ok(Self {
@@ -115,6 +116,25 @@ impl StreamingTableExec {
115116
})
116117
}
117118

119+
/// Declares the output partitioning of this stream.
120+
///
121+
/// `output_partitioning` must describe this plan's current output and have
122+
/// the same number of partitions as the stream.
123+
pub fn with_output_partitioning(
124+
mut self,
125+
output_partitioning: Partitioning,
126+
) -> Result<Self> {
127+
if output_partitioning.partition_count() != self.partitions.len() {
128+
return plan_err!(
129+
"Output partitioning has {} partitions but stream has {} partitions",
130+
output_partitioning.partition_count(),
131+
self.partitions.len()
132+
);
133+
}
134+
Arc::make_mut(&mut self.cache).partitioning = output_partitioning;
135+
Ok(self)
136+
}
137+
118138
pub fn partitions(&self) -> &Vec<Arc<dyn PartitionStream>> {
119139
&self.partitions
120140
}
@@ -147,14 +167,12 @@ impl StreamingTableExec {
147167
fn compute_properties(
148168
schema: SchemaRef,
149169
orderings: Vec<LexOrdering>,
150-
partitions: &[Arc<dyn PartitionStream>],
170+
output_partitioning: Partitioning,
151171
infinite: bool,
152172
) -> PlanProperties {
153173
// Calculate equivalence properties:
154174
let eq_properties = EquivalenceProperties::new_with_orderings(schema, orderings);
155175

156-
// Get output partitioning:
157-
let output_partitioning = Partitioning::UnknownPartitioning(partitions.len());
158176
let boundedness = if infinite {
159177
Boundedness::Unbounded {
160178
requires_infinite_memory: false,
@@ -204,6 +222,16 @@ impl DisplayAs for StreamingTableExec {
204222
if let Some(fetch) = self.limit {
205223
write!(f, ", fetch={fetch}")?;
206224
}
225+
if !matches!(
226+
self.cache.output_partitioning(),
227+
Partitioning::UnknownPartitioning(_)
228+
) {
229+
write!(
230+
f,
231+
", output_partitioning={}",
232+
self.cache.output_partitioning()
233+
)?;
234+
}
207235

208236
display_orderings(f, &self.projected_output_ordering)?;
209237

@@ -306,6 +334,17 @@ impl ExecutionPlan for StreamingTableExec {
306334
};
307335
lex_orderings.push(ordering);
308336
}
337+
let projection_mapping = ProjectionMapping::try_new(
338+
projection
339+
.expr()
340+
.iter()
341+
.map(|expr| (Arc::clone(&expr.expr), expr.alias.clone())),
342+
&self.schema(),
343+
)?;
344+
let output_partitioning = self
345+
.cache
346+
.output_partitioning()
347+
.project(&projection_mapping, self.cache.equivalence_properties());
309348

310349
StreamingTableExec::try_new(
311350
Arc::clone(self.partition_schema()),
@@ -315,6 +354,7 @@ impl ExecutionPlan for StreamingTableExec {
315354
self.is_infinite(),
316355
self.limit(),
317356
)
357+
.and_then(|exec| exec.with_output_partitioning(output_partitioning))
318358
.map(|e| Some(Arc::new(e) as _))
319359
}
320360

0 commit comments

Comments
 (0)