blob: 7469c3af9344c3e619bf9e5e25a4a6377993ccb5 [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.
//! [`LimitPushdown`] pushes `LIMIT` down through `ExecutionPlan`s to reduce
//! data transfer as much as possible.
use std::fmt::Debug;
use std::sync::Arc;
use crate::PhysicalOptimizerRule;
use datafusion_common::config::ConfigOptions;
use datafusion_common::error::Result;
use datafusion_common::tree_node::{Transformed, TreeNodeRecursion};
use datafusion_common::utils::combine_limit;
use datafusion_physical_plan::coalesce_partitions::CoalescePartitionsExec;
use datafusion_physical_plan::limit::{GlobalLimitExec, LocalLimitExec};
use datafusion_physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec;
use datafusion_physical_plan::{ExecutionPlan, ExecutionPlanProperties};
/// This rule inspects [`ExecutionPlan`]'s and pushes down the fetch limit from
/// the parent to the child if applicable.
#[derive(Default, Debug)]
pub struct LimitPushdown {}
/// This is a "data class" we use within the [`LimitPushdown`] rule to push
/// down [`LimitExec`] in the plan. GlobalRequirements are hold as a rule-wide state
/// and holds the fetch and skip information. The struct also has a field named
/// satisfied which means if the "current" plan is valid in terms of limits or not.
///
/// For example: If the plan is satisfied with current fetch info, we decide to not add a LocalLimit
///
/// [`LimitPushdown`]: crate::limit_pushdown::LimitPushdown
/// [`LimitExec`]: crate::limit_pushdown::LimitExec
#[derive(Default, Clone, Debug)]
pub struct GlobalRequirements {
fetch: Option<usize>,
skip: usize,
satisfied: bool,
}
impl LimitPushdown {
#[allow(missing_docs)]
pub fn new() -> Self {
Self {}
}
}
impl PhysicalOptimizerRule for LimitPushdown {
fn optimize(
&self,
plan: Arc<dyn ExecutionPlan>,
_config: &ConfigOptions,
) -> Result<Arc<dyn ExecutionPlan>> {
let global_state = GlobalRequirements {
fetch: None,
skip: 0,
satisfied: false,
};
pushdown_limits(plan, global_state)
}
fn name(&self) -> &str {
"LimitPushdown"
}
fn schema_check(&self) -> bool {
true
}
}
/// This enumeration makes `skip` and `fetch` calculations easier by providing
/// a single API for both local and global limit operators.
#[derive(Debug)]
pub enum LimitExec {
Global(GlobalLimitExec),
Local(LocalLimitExec),
}
impl LimitExec {
fn input(&self) -> &Arc<dyn ExecutionPlan> {
match self {
Self::Global(global) => global.input(),
Self::Local(local) => local.input(),
}
}
fn fetch(&self) -> Option<usize> {
match self {
Self::Global(global) => global.fetch(),
Self::Local(local) => Some(local.fetch()),
}
}
fn skip(&self) -> usize {
match self {
Self::Global(global) => global.skip(),
Self::Local(_) => 0,
}
}
}
impl From<LimitExec> for Arc<dyn ExecutionPlan> {
fn from(limit_exec: LimitExec) -> Self {
match limit_exec {
LimitExec::Global(global) => Arc::new(global),
LimitExec::Local(local) => Arc::new(local),
}
}
}
/// This function is the main helper function of the `LimitPushDown` rule.
/// The helper takes an `ExecutionPlan` and a global (algorithm) state which is
/// an instance of `GlobalRequirements` and modifies these parameters while
/// checking if the limits can be pushed down or not.
///
/// If a limit is encountered, a [`TreeNodeRecursion::Stop`] is returned. Otherwise,
/// return a [`TreeNodeRecursion::Continue`].
pub fn pushdown_limit_helper(
mut pushdown_plan: Arc<dyn ExecutionPlan>,
mut global_state: GlobalRequirements,
) -> Result<(Transformed<Arc<dyn ExecutionPlan>>, GlobalRequirements)> {
// Extract limit, if exist, and return child inputs.
if let Some(limit_exec) = extract_limit(&pushdown_plan) {
// If we have fetch/skip info in the global state already, we need to
// decide which one to continue with:
let (skip, fetch) = combine_limit(
global_state.skip,
global_state.fetch,
limit_exec.skip(),
limit_exec.fetch(),
);
global_state.skip = skip;
global_state.fetch = fetch;
// Now the global state has the most recent information, we can remove
// the `LimitExec` plan. We will decide later if we should add it again
// or not.
return Ok((
Transformed {
data: Arc::clone(limit_exec.input()),
transformed: true,
tnr: TreeNodeRecursion::Stop,
},
global_state,
));
}
// If we have a non-limit operator with fetch capability, update global
// state as necessary:
if pushdown_plan.fetch().is_some() {
if global_state.fetch.is_none() {
global_state.satisfied = true;
}
(global_state.skip, global_state.fetch) = combine_limit(
global_state.skip,
global_state.fetch,
0,
pushdown_plan.fetch(),
);
}
let Some(global_fetch) = global_state.fetch else {
// There's no valid fetch information, exit early:
return if global_state.skip > 0 && !global_state.satisfied {
// There might be a case with only offset, if so add a global limit:
global_state.satisfied = true;
Ok((
Transformed::yes(add_global_limit(
pushdown_plan,
global_state.skip,
None,
)),
global_state,
))
} else {
// There's no info on offset or fetch, nothing to do:
Ok((Transformed::no(pushdown_plan), global_state))
};
};
let skip_and_fetch = Some(global_fetch + global_state.skip);
if pushdown_plan.supports_limit_pushdown() {
if !combines_input_partitions(&pushdown_plan) {
// We have information in the global state and the plan pushes down,
// continue:
Ok((Transformed::no(pushdown_plan), global_state))
} else if let Some(plan_with_fetch) = pushdown_plan.with_fetch(skip_and_fetch) {
// This plan is combining input partitions, so we need to add the
// fetch info to plan if possible. If not, we must add a `LimitExec`
// with the information from the global state.
let mut new_plan = plan_with_fetch;
// Execution plans can't (yet) handle skip, so if we have one,
// we still need to add a global limit
if global_state.skip > 0 {
new_plan =
add_global_limit(new_plan, global_state.skip, global_state.fetch);
}
global_state.fetch = skip_and_fetch;
global_state.skip = 0;
global_state.satisfied = true;
Ok((Transformed::yes(new_plan), global_state))
} else if global_state.satisfied {
// If the plan is already satisfied, do not add a limit:
Ok((Transformed::no(pushdown_plan), global_state))
} else {
global_state.satisfied = true;
Ok((
Transformed::yes(add_limit(
pushdown_plan,
global_state.skip,
global_fetch,
)),
global_state,
))
}
} else {
// The plan does not support push down and it is not a limit. We will need
// to add a limit or a fetch. If the plan is already satisfied, we will try
// to add the fetch info and return the plan.
// There's no push down, change fetch & skip to default values:
let global_skip = global_state.skip;
global_state.fetch = None;
global_state.skip = 0;
let maybe_fetchable = pushdown_plan.with_fetch(skip_and_fetch);
if global_state.satisfied {
if let Some(plan_with_fetch) = maybe_fetchable {
Ok((Transformed::yes(plan_with_fetch), global_state))
} else {
Ok((Transformed::no(pushdown_plan), global_state))
}
} else {
global_state.satisfied = true;
pushdown_plan = if let Some(plan_with_fetch) = maybe_fetchable {
if global_skip > 0 {
add_global_limit(plan_with_fetch, global_skip, Some(global_fetch))
} else {
plan_with_fetch
}
} else {
add_limit(pushdown_plan, global_skip, global_fetch)
};
Ok((Transformed::yes(pushdown_plan), global_state))
}
}
}
/// Pushes down the limit through the plan.
pub(crate) fn pushdown_limits(
pushdown_plan: Arc<dyn ExecutionPlan>,
global_state: GlobalRequirements,
) -> Result<Arc<dyn ExecutionPlan>> {
// Call pushdown_limit_helper.
// This will either extract the limit node (returning the child), or apply the limit pushdown.
let (mut new_node, mut global_state) =
pushdown_limit_helper(pushdown_plan, global_state)?;
// While limits exist, continue combining the global_state.
while new_node.tnr == TreeNodeRecursion::Stop {
(new_node, global_state) = pushdown_limit_helper(new_node.data, global_state)?;
}
// Apply pushdown limits in children
let children = new_node.data.children();
let new_children = children
.into_iter()
.map(|child| {
pushdown_limits(Arc::<dyn ExecutionPlan>::clone(child), global_state.clone())
})
.collect::<Result<_>>()?;
new_node.data.with_new_children(new_children)
}
/// Transforms the [`ExecutionPlan`] into a [`LimitExec`] if it is a
/// [`GlobalLimitExec`] or a [`LocalLimitExec`].
fn extract_limit(plan: &Arc<dyn ExecutionPlan>) -> Option<LimitExec> {
if let Some(global_limit) = plan.as_any().downcast_ref::<GlobalLimitExec>() {
Some(LimitExec::Global(GlobalLimitExec::new(
Arc::clone(global_limit.input()),
global_limit.skip(),
global_limit.fetch(),
)))
} else {
plan.as_any()
.downcast_ref::<LocalLimitExec>()
.map(|local_limit| {
LimitExec::Local(LocalLimitExec::new(
Arc::clone(local_limit.input()),
local_limit.fetch(),
))
})
}
}
/// Checks if the given plan combines input partitions.
fn combines_input_partitions(plan: &Arc<dyn ExecutionPlan>) -> bool {
let plan = plan.as_any();
plan.is::<CoalescePartitionsExec>() || plan.is::<SortPreservingMergeExec>()
}
/// Adds a limit to the plan, chooses between global and local limits based on
/// skip value and the number of partitions.
fn add_limit(
pushdown_plan: Arc<dyn ExecutionPlan>,
skip: usize,
fetch: usize,
) -> Arc<dyn ExecutionPlan> {
if skip > 0 || pushdown_plan.output_partitioning().partition_count() == 1 {
add_global_limit(pushdown_plan, skip, Some(fetch))
} else {
Arc::new(LocalLimitExec::new(pushdown_plan, fetch + skip)) as _
}
}
/// Adds a global limit to the plan.
fn add_global_limit(
pushdown_plan: Arc<dyn ExecutionPlan>,
skip: usize,
fetch: Option<usize>,
) -> Arc<dyn ExecutionPlan> {
Arc::new(GlobalLimitExec::new(pushdown_plan, skip, fetch)) as _
}
// See tests in datafusion/core/tests/physical_optimizer