blob: d6b2b1b046f75fe8bf862cc7db19cf8ce782dfa2 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! [`Partitioning`] and [`Distribution`] for `ExecutionPlans`
use crate::{
equivalence::ProjectionMapping, expressions::UnKnownColumn, physical_exprs_equal,
EquivalenceProperties, PhysicalExpr,
};
use datafusion_physical_expr_common::physical_expr::format_physical_expr_list;
use std::fmt;
use std::fmt::Display;
use std::sync::Arc;
/// Output partitioning supported by [`ExecutionPlan`]s.
///
/// Calling [`ExecutionPlan::execute`] produce one or more independent streams of
/// [`RecordBatch`]es in parallel, referred to as partitions. The streams are Rust
/// `async` [`Stream`]s (a special kind of future). The number of output
/// partitions varies based on the input and the operation performed.
///
/// For example, an `ExecutionPlan` that has output partitioning of 3 will
/// produce 3 distinct output streams as the result of calling
/// `ExecutionPlan::execute(0)`, `ExecutionPlan::execute(1)`, and
/// `ExecutionPlan::execute(2)`, as shown below:
///
/// ```text
/// ... ... ...
/// ... ▲ ▲ ▲
/// │ │ │
/// ▲ │ │ │
/// │ │ │ │
/// │ ┌───┴────┐ ┌───┴────┐ ┌───┴────┐
/// ┌────────────────────┐ │ Stream │ │ Stream │ │ Stream │
/// │ ExecutionPlan │ │ (0) │ │ (1) │ │ (2) │
/// └────────────────────┘ └────────┘ └────────┘ └────────┘
/// ▲ ▲ ▲ ▲
/// │ │ │ │
/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ │
/// Input │ │ │ │
/// └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ │
/// ▲ ┌ ─ ─ ─ ─ ┌ ─ ─ ─ ─ ┌ ─ ─ ─ ─
/// │ Input │ Input │ Input │
/// │ │ Stream │ Stream │ Stream
/// (0) │ (1) │ (2) │
/// ... └ ─ ▲ ─ ─ └ ─ ▲ ─ ─ └ ─ ▲ ─ ─
/// │ │ │
/// │ │ │
/// │ │ │
///
/// ExecutionPlan with 1 input 3 (async) streams, one for each
/// that has 3 partitions, which itself output partition
/// has 3 output partitions
/// ```
///
/// It is common (but not required) that an `ExecutionPlan` has the same number
/// of input partitions as output partitions. However, some plans have different
/// numbers such as the `RepartitionExec` that redistributes batches from some
/// number of inputs to some number of outputs
///
/// ```text
/// ... ... ... ...
///
/// ▲ ▲ ▲
/// ▲ │ │ │
/// │ │ │ │
/// ┌────────┴───────────┐ │ │ │
/// │ RepartitionExec │ ┌────┴───┐ ┌────┴───┐ ┌────┴───┐
/// └────────────────────┘ │ Stream │ │ Stream │ │ Stream │
/// ▲ │ (0) │ │ (1) │ │ (2) │
/// │ └────────┘ └────────┘ └────────┘
/// │ ▲ ▲ ▲
/// ... │ │ │
/// └──────────┐│┌──────────┘
/// │││
/// │││
/// RepartitionExec with 1 input
/// partition and 3 output partitions 3 (async) streams, that internally
/// pull from the same input stream
/// ...
/// ```
///
/// # Additional Examples
///
/// A simple `FileScanExec` might produce one output stream (partition) for each
/// file (note the actual DataFusion file scanners can read individual files in
/// parallel, potentially producing multiple partitions per file)
///
/// Plans such as `SortPreservingMerge` produce a single output stream
/// (1 output partition) by combining some number of input streams (input partitions)
///
/// Plans such as `FilterExec` produce the same number of output streams
/// (partitions) as input streams (partitions).
///
/// [`RecordBatch`]: arrow::record_batch::RecordBatch
/// [`ExecutionPlan::execute`]: https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html#tymethod.execute
/// [`ExecutionPlan`]: https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html
/// [`Stream`]: https://docs.rs/futures/latest/futures/stream/trait.Stream.html
#[derive(Debug, Clone)]
pub enum Partitioning {
/// Allocate batches using a round-robin algorithm and the specified number of partitions
RoundRobinBatch(usize),
/// Allocate rows based on a hash of one of more expressions and the specified number of
/// partitions
Hash(Vec<Arc<dyn PhysicalExpr>>, usize),
/// Unknown partitioning scheme with a known number of partitions
UnknownPartitioning(usize),
}
impl Display for Partitioning {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Partitioning::RoundRobinBatch(size) => write!(f, "RoundRobinBatch({size})"),
Partitioning::Hash(phy_exprs, size) => {
let phy_exprs_str = phy_exprs
.iter()
.map(|e| format!("{e}"))
.collect::<Vec<String>>()
.join(", ");
write!(f, "Hash([{phy_exprs_str}], {size})")
}
Partitioning::UnknownPartitioning(size) => {
write!(f, "UnknownPartitioning({size})")
}
}
}
}
impl Partitioning {
/// Returns the number of partitions in this partitioning scheme
pub fn partition_count(&self) -> usize {
use Partitioning::*;
match self {
RoundRobinBatch(n) | Hash(_, n) | UnknownPartitioning(n) => *n,
}
}
/// Returns true when the guarantees made by this [`Partitioning`] are sufficient to
/// satisfy the partitioning scheme mandated by the `required` [`Distribution`].
pub fn satisfy(
&self,
required: &Distribution,
eq_properties: &EquivalenceProperties,
) -> bool {
match required {
Distribution::UnspecifiedDistribution => true,
Distribution::SinglePartition if self.partition_count() == 1 => true,
// When partition count is 1, hash requirement is satisfied.
Distribution::HashPartitioned(_) if self.partition_count() == 1 => true,
Distribution::HashPartitioned(required_exprs) => {
match self {
// Here we do not check the partition count for hash partitioning and assumes the partition count
// and hash functions in the system are the same. In future if we plan to support storage partition-wise joins,
// then we need to have the partition count and hash functions validation.
Partitioning::Hash(partition_exprs, _) => {
let fast_match =
physical_exprs_equal(required_exprs, partition_exprs);
// If the required exprs do not match, need to leverage the eq_properties provided by the child
// and normalize both exprs based on the equivalent groups.
if !fast_match {
let eq_groups = eq_properties.eq_group();
if !eq_groups.is_empty() {
let normalized_required_exprs = required_exprs
.iter()
.map(|e| eq_groups.normalize_expr(Arc::clone(e)))
.collect::<Vec<_>>();
let normalized_partition_exprs = partition_exprs
.iter()
.map(|e| eq_groups.normalize_expr(Arc::clone(e)))
.collect::<Vec<_>>();
return physical_exprs_equal(
&normalized_required_exprs,
&normalized_partition_exprs,
);
}
}
fast_match
}
_ => false,
}
}
_ => false,
}
}
/// Calculate the output partitioning after applying the given projection.
pub fn project(
&self,
mapping: &ProjectionMapping,
input_eq_properties: &EquivalenceProperties,
) -> Self {
if let Partitioning::Hash(exprs, part) = self {
let normalized_exprs = input_eq_properties
.project_expressions(exprs, mapping)
.zip(exprs)
.map(|(proj_expr, expr)| {
proj_expr.unwrap_or_else(|| {
Arc::new(UnKnownColumn::new(&expr.to_string()))
})
})
.collect();
Partitioning::Hash(normalized_exprs, *part)
} else {
self.clone()
}
}
}
impl PartialEq for Partitioning {
fn eq(&self, other: &Partitioning) -> bool {
match (self, other) {
(
Partitioning::RoundRobinBatch(count1),
Partitioning::RoundRobinBatch(count2),
) if count1 == count2 => true,
(Partitioning::Hash(exprs1, count1), Partitioning::Hash(exprs2, count2))
if physical_exprs_equal(exprs1, exprs2) && (count1 == count2) =>
{
true
}
_ => false,
}
}
}
/// How data is distributed amongst partitions. See [`Partitioning`] for more
/// details.
#[derive(Debug, Clone)]
pub enum Distribution {
/// Unspecified distribution
UnspecifiedDistribution,
/// A single partition is required
SinglePartition,
/// Requires children to be distributed in such a way that the same
/// values of the keys end up in the same partition
HashPartitioned(Vec<Arc<dyn PhysicalExpr>>),
}
impl Distribution {
/// Creates a `Partitioning` that satisfies this `Distribution`
pub fn create_partitioning(self, partition_count: usize) -> Partitioning {
match self {
Distribution::UnspecifiedDistribution => {
Partitioning::UnknownPartitioning(partition_count)
}
Distribution::SinglePartition => Partitioning::UnknownPartitioning(1),
Distribution::HashPartitioned(expr) => {
Partitioning::Hash(expr, partition_count)
}
}
}
}
impl Display for Distribution {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Distribution::UnspecifiedDistribution => write!(f, "Unspecified"),
Distribution::SinglePartition => write!(f, "SinglePartition"),
Distribution::HashPartitioned(exprs) => {
write!(f, "HashPartitioned[{}])", format_physical_expr_list(exprs))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expressions::Column;
use arrow::datatypes::{DataType, Field, Schema};
use datafusion_common::Result;
#[test]
fn partitioning_satisfy_distribution() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("column_1", DataType::Int64, false),
Field::new("column_2", DataType::Utf8, false),
]));
let partition_exprs1: Vec<Arc<dyn PhysicalExpr>> = vec![
Arc::new(Column::new_with_schema("column_1", &schema).unwrap()),
Arc::new(Column::new_with_schema("column_2", &schema).unwrap()),
];
let partition_exprs2: Vec<Arc<dyn PhysicalExpr>> = vec![
Arc::new(Column::new_with_schema("column_2", &schema).unwrap()),
Arc::new(Column::new_with_schema("column_1", &schema).unwrap()),
];
let distribution_types = vec![
Distribution::UnspecifiedDistribution,
Distribution::SinglePartition,
Distribution::HashPartitioned(partition_exprs1.clone()),
];
let single_partition = Partitioning::UnknownPartitioning(1);
let unspecified_partition = Partitioning::UnknownPartitioning(10);
let round_robin_partition = Partitioning::RoundRobinBatch(10);
let hash_partition1 = Partitioning::Hash(partition_exprs1, 10);
let hash_partition2 = Partitioning::Hash(partition_exprs2, 10);
let eq_properties = EquivalenceProperties::new(schema);
for distribution in distribution_types {
let result = (
single_partition.satisfy(&distribution, &eq_properties),
unspecified_partition.satisfy(&distribution, &eq_properties),
round_robin_partition.satisfy(&distribution, &eq_properties),
hash_partition1.satisfy(&distribution, &eq_properties),
hash_partition2.satisfy(&distribution, &eq_properties),
);
match distribution {
Distribution::UnspecifiedDistribution => {
assert_eq!(result, (true, true, true, true, true))
}
Distribution::SinglePartition => {
assert_eq!(result, (true, false, false, false, false))
}
Distribution::HashPartitioned(_) => {
assert_eq!(result, (true, false, false, true, false))
}
}
}
Ok(())
}
}