Skip to main content

datafusion_physical_optimizer/
output_requirements.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! The GlobalOrderRequire optimizer rule either:
19//! - Adds an auxiliary `OutputRequirementExec` operator to keep track of global
20//!   ordering and distribution requirement across rules, or
21//! - Removes the auxiliary `OutputRequirementExec` operator from the physical plan.
22//!   Since the `OutputRequirementExec` operator is only a helper operator, it
23//!   shouldn't occur in the final plan (i.e. the executed plan).
24
25use std::sync::Arc;
26
27use crate::PhysicalOptimizerRule;
28
29use datafusion_common::config::ConfigOptions;
30use datafusion_common::tree_node::{
31    Transformed, TransformedResult, TreeNode, TreeNodeRecursion,
32};
33use datafusion_common::{Result, Statistics, internal_err};
34use datafusion_execution::TaskContext;
35use datafusion_physical_expr::Distribution;
36use datafusion_physical_expr_common::sort_expr::OrderingRequirements;
37use datafusion_physical_plan::execution_plan::{
38    Boundedness, replace_children_if_necessary,
39};
40use datafusion_physical_plan::projection::{
41    ProjectionExec, make_with_child, update_expr, update_ordering_requirement,
42};
43use datafusion_physical_plan::scalar_subquery::ScalarSubqueryExec;
44use datafusion_physical_plan::sorts::sort::SortExec;
45use datafusion_physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec;
46use datafusion_physical_plan::{
47    ChildStats, ChildrenPropertiesMode, DisplayAs, DisplayFormatType, ExecutionPlan,
48    ExecutionPlanProperties, PlanProperties, ReplaceChildrenOptions,
49    SendableRecordBatchStream, StatisticsArgs,
50};
51
52/// This rule either adds or removes [`OutputRequirements`]s to/from the physical
53/// plan according to its `mode` attribute, which is set by the constructors
54/// `new_add_mode` and `new_remove_mode`. With this rule, we can keep track of
55/// the global requirements (ordering and distribution) across rules.
56///
57/// The primary use case of this node and rule is to specify and preserve the desired output
58/// ordering and distribution the entire plan. When sending to a single client, a single partition may
59/// be desirable, but when sending to a multi-partitioned writer, keeping multiple partitions may be
60/// better.
61#[derive(Debug)]
62pub struct OutputRequirements {
63    mode: RuleMode,
64}
65
66impl OutputRequirements {
67    /// Create a new rule which works in `Add` mode; i.e. it simply adds a
68    /// top-level [`OutputRequirementExec`] into the physical plan to keep track
69    /// of global ordering and distribution requirements if there are any.
70    /// Note that this rule should run at the beginning. It is idempotent: when
71    /// invoked on a plan that already contains an `OutputRequirementExec` (at
72    /// the root or below it), it returns the plan unchanged.
73    pub fn new_add_mode() -> Self {
74        Self {
75            mode: RuleMode::Add,
76        }
77    }
78
79    /// Create a new rule which works in `Remove` mode; i.e. it simply removes
80    /// the top-level [`OutputRequirementExec`] from the physical plan if there is
81    /// any. We do this because a `OutputRequirementExec` is an ancillary,
82    /// non-executable operator whose sole purpose is to track global
83    /// requirements during optimization. Therefore, a
84    /// `OutputRequirementExec` should not appear in the final plan.
85    pub fn new_remove_mode() -> Self {
86        Self {
87            mode: RuleMode::Remove,
88        }
89    }
90}
91
92#[derive(Debug, Ord, PartialOrd, PartialEq, Eq, Hash)]
93enum RuleMode {
94    Add,
95    Remove,
96}
97
98/// An ancillary, non-executable operator whose sole purpose is to track global
99/// requirements during optimization. It imposes
100/// - the ordering requirement in its `order_requirement` attribute.
101/// - the distribution requirement in its `dist_requirement` attribute.
102///
103/// See [`OutputRequirements`] for more details
104#[derive(Debug)]
105pub struct OutputRequirementExec {
106    input: Arc<dyn ExecutionPlan>,
107    order_requirement: Option<OrderingRequirements>,
108    dist_requirement: Distribution,
109    cache: Arc<PlanProperties>,
110    fetch: Option<usize>,
111}
112
113impl OutputRequirementExec {
114    pub fn new(
115        input: Arc<dyn ExecutionPlan>,
116        requirements: Option<OrderingRequirements>,
117        dist_requirement: Distribution,
118        fetch: Option<usize>,
119    ) -> Self {
120        let cache = Self::compute_properties(&input, &fetch);
121        Self {
122            input,
123            order_requirement: requirements,
124            dist_requirement,
125            cache: Arc::new(cache),
126            fetch,
127        }
128    }
129
130    pub fn input(&self) -> Arc<dyn ExecutionPlan> {
131        Arc::clone(&self.input)
132    }
133
134    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
135    fn compute_properties(
136        input: &Arc<dyn ExecutionPlan>,
137        fetch: &Option<usize>,
138    ) -> PlanProperties {
139        let boundedness = if fetch.is_some() {
140            Boundedness::Bounded
141        } else {
142            input.boundedness()
143        };
144
145        PlanProperties::new(
146            input.equivalence_properties().clone(), // Equivalence Properties
147            input.output_partitioning().clone(),    // Output Partitioning
148            input.pipeline_behavior(),              // Pipeline Behavior
149            boundedness,                            // Boundedness
150        )
151    }
152
153    /// Get fetch
154    pub fn fetch(&self) -> Option<usize> {
155        self.fetch
156    }
157}
158
159impl DisplayAs for OutputRequirementExec {
160    fn fmt_as(
161        &self,
162        t: DisplayFormatType,
163        f: &mut std::fmt::Formatter,
164    ) -> std::fmt::Result {
165        match t {
166            DisplayFormatType::Default | DisplayFormatType::Verbose => {
167                let order_cols = self
168                    .order_requirement
169                    .as_ref()
170                    .map(|reqs| reqs.first())
171                    .map(|lex| {
172                        let pairs: Vec<String> = lex
173                            .iter()
174                            .map(|req| {
175                                let direction = req
176                                    .options
177                                    .as_ref()
178                                    .map(
179                                        |opt| if opt.descending { "desc" } else { "asc" },
180                                    )
181                                    .unwrap_or("unspecified");
182                                format!("({}, {direction})", req.expr)
183                            })
184                            .collect();
185                        format!("[{}]", pairs.join(", "))
186                    })
187                    .unwrap_or_else(|| "[]".to_string());
188
189                write!(
190                    f,
191                    "OutputRequirementExec: order_by={}, dist_by={}",
192                    order_cols, self.dist_requirement
193                )
194            }
195            DisplayFormatType::TreeRender => {
196                write!(f, "")
197            }
198        }
199    }
200}
201
202impl ExecutionPlan for OutputRequirementExec {
203    fn name(&self) -> &'static str {
204        "OutputRequirementExec"
205    }
206
207    fn properties(&self) -> &Arc<PlanProperties> {
208        &self.cache
209    }
210
211    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
212        vec![false]
213    }
214
215    fn required_input_distribution(&self) -> Vec<Distribution> {
216        self.input_distribution_requirements().into_per_child()
217    }
218
219    fn input_distribution_requirements(
220        &self,
221    ) -> datafusion_physical_plan::InputDistributionRequirements {
222        datafusion_physical_plan::InputDistributionRequirements::new(vec![
223            self.dist_requirement.clone(),
224        ])
225    }
226
227    fn maintains_input_order(&self) -> Vec<bool> {
228        vec![true]
229    }
230
231    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
232        vec![&self.input]
233    }
234
235    fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
236        vec![self.order_requirement.clone()]
237    }
238
239    fn replace_children(
240        self: Arc<Self>,
241        mut children: Vec<Arc<dyn ExecutionPlan>>,
242        _: ReplaceChildrenOptions,
243    ) -> Result<Arc<dyn ExecutionPlan>> {
244        Ok(Arc::new(Self::new(
245            children.remove(0), // has a single child
246            self.order_requirement.clone(),
247            self.dist_requirement.clone(),
248            self.fetch,
249        )))
250    }
251
252    fn with_new_children(
253        self: Arc<Self>,
254        children: Vec<Arc<dyn ExecutionPlan>>,
255    ) -> Result<Arc<dyn ExecutionPlan>> {
256        self.replace_children(
257            children,
258            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
259        )
260    }
261
262    fn execute(
263        &self,
264        _partition: usize,
265        _context: Arc<TaskContext>,
266    ) -> Result<SendableRecordBatchStream> {
267        unreachable!();
268    }
269
270    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
271        vec![ChildStats::At(partition)]
272    }
273
274    fn statistics_from_inputs(
275        &self,
276        input_stats: &[Arc<Statistics>],
277        _args: &StatisticsArgs,
278    ) -> Result<Arc<Statistics>> {
279        Ok(Arc::clone(&input_stats[0]))
280    }
281
282    #[expect(
283        deprecated,
284        reason = "HashPartitioned is accepted during the KeyPartitioned migration"
285    )]
286    fn try_swapping_with_projection(
287        &self,
288        projection: &ProjectionExec,
289    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
290        // If the projection does not narrow the schema, we should not try to push it down:
291        let proj_exprs = projection.expr();
292        if proj_exprs.len() >= projection.input().schema().fields().len() {
293            return Ok(None);
294        }
295
296        let mut requirements = self.required_input_ordering().swap_remove(0);
297        if let Some(reqs) = requirements {
298            let mut updated_reqs = vec![];
299            let (lexes, soft) = reqs.into_alternatives();
300            for lex in lexes.into_iter() {
301                let Some(updated_lex) = update_ordering_requirement(lex, proj_exprs)?
302                else {
303                    return Ok(None);
304                };
305                updated_reqs.push(updated_lex);
306            }
307            requirements = OrderingRequirements::new_alternatives(updated_reqs, soft);
308        }
309
310        let input_distributions = self.input_distribution_requirements();
311        let dist_req = match input_distributions.child_distribution(0) {
312            Some(
313                Distribution::HashPartitioned(exprs)
314                | Distribution::KeyPartitioned(exprs),
315            ) => {
316                let mut updated_exprs = vec![];
317                for expr in exprs {
318                    let Some(new_expr) = update_expr(expr, projection.expr(), false)?
319                    else {
320                        return Ok(None);
321                    };
322                    updated_exprs.push(new_expr);
323                }
324                Distribution::KeyPartitioned(updated_exprs)
325            }
326            Some(dist) => dist.clone(),
327            None => {
328                return internal_err!(
329                    "OutputRequirementExec missing input distribution requirement"
330                );
331            }
332        };
333
334        make_with_child(projection, &self.input()).map(|input| {
335            let e = OutputRequirementExec::new(input, requirements, dist_req, self.fetch);
336            Some(Arc::new(e) as _)
337        })
338    }
339
340    fn fetch(&self) -> Option<usize> {
341        self.fetch
342    }
343
344    fn apply_expressions(
345        &self,
346        _f: &mut dyn FnMut(
347            &Arc<dyn datafusion_physical_expr_common::physical_expr::PhysicalExpr>,
348        ) -> Result<TreeNodeRecursion>,
349    ) -> Result<TreeNodeRecursion> {
350        Ok(TreeNodeRecursion::Continue)
351    }
352}
353
354impl PhysicalOptimizerRule for OutputRequirements {
355    fn optimize(
356        &self,
357        plan: Arc<dyn ExecutionPlan>,
358        _config: &ConfigOptions,
359    ) -> Result<Arc<dyn ExecutionPlan>> {
360        match self.mode {
361            RuleMode::Add => require_top_ordering(plan),
362            RuleMode::Remove => plan
363                .transform_up(|plan| {
364                    if let Some(sort_req) = plan.downcast_ref::<OutputRequirementExec>() {
365                        Ok(Transformed::yes(sort_req.input()))
366                    } else {
367                        Ok(Transformed::no(plan))
368                    }
369                })
370                .data(),
371        }
372    }
373
374    fn name(&self) -> &str {
375        "OutputRequirements"
376    }
377
378    fn schema_check(&self) -> bool {
379        true
380    }
381}
382
383/// This functions adds ancillary `OutputRequirementExec` to the physical plan, so that
384/// global requirements are not lost during optimization.
385///
386/// Idempotent: re-running this rule (as adaptive execution in datafusion-ballista
387/// AQE does after every completed stage, see datafusion-ballista#1359) does not
388/// stack wrappers, whether the previously-added `OutputRequirementExec` sits at
389/// the root (handled here) or below it (handled in `require_top_ordering_helper`).
390fn require_top_ordering(plan: Arc<dyn ExecutionPlan>) -> Result<Arc<dyn ExecutionPlan>> {
391    if plan.downcast_ref::<OutputRequirementExec>().is_some() {
392        return Ok(plan);
393    }
394    let (new_plan, is_changed) = require_top_ordering_helper(plan)?;
395    if is_changed {
396        Ok(new_plan)
397    } else {
398        // Add `OutputRequirementExec` to the top, with no specified ordering and distribution requirement.
399        Ok(Arc::new(OutputRequirementExec::new(
400            new_plan,
401            // there is no ordering requirement
402            None,
403            Distribution::UnspecifiedDistribution,
404            None,
405        )) as _)
406    }
407}
408
409/// Which child (if any) `require_top_ordering_helper` should descend into when
410/// searching for the operator that establishes the global ordering.
411fn output_requirement_child(plan: &dyn ExecutionPlan) -> Option<usize> {
412    if plan.children().len() == 1 {
413        Some(0)
414    } else if plan.downcast_ref::<ScalarSubqueryExec>().is_some() {
415        // `ScalarSubqueryExec` is multi-child but order-transparent on child 0
416        // (the main input); its other children are subquery plans that don't
417        // affect output ordering, so descend into child 0. Without this the
418        // search stops here and loses the query's global ORDER BY.
419        Some(0)
420    } else {
421        None
422    }
423}
424
425/// Helper function that adds an ancillary `OutputRequirementExec` to the given plan.
426/// First entry in the tuple is resulting plan, second entry indicates whether any
427/// `OutputRequirementExec` is added to the plan.
428fn require_top_ordering_helper(
429    plan: Arc<dyn ExecutionPlan>,
430) -> Result<(Arc<dyn ExecutionPlan>, bool)> {
431    // A previous run of this rule already captured the ordering requirement at
432    // this node. Report it as already handled.
433    if plan.downcast_ref::<OutputRequirementExec>().is_some() {
434        return Ok((plan, true));
435    }
436
437    // Global ordering defines desired ordering in the final result.
438    if let Some(sort_exec) = plan.downcast_ref::<SortExec>() {
439        // In case of constant columns, output ordering of the `SortExec` would
440        // be an empty set. Therefore; we check the sort expression field to
441        // assign the requirements.
442        let req_dist = sort_exec
443            .input_distribution_requirements()
444            .into_per_child()
445            .swap_remove(0);
446        let req_ordering = sort_exec.expr();
447        let reqs = OrderingRequirements::from(req_ordering.clone());
448        let fetch = sort_exec.fetch();
449
450        Ok((
451            Arc::new(OutputRequirementExec::new(
452                plan,
453                Some(reqs),
454                req_dist,
455                fetch,
456            )) as _,
457            true,
458        ))
459    } else if let Some(spm) = plan.downcast_ref::<SortPreservingMergeExec>() {
460        let reqs = OrderingRequirements::from(spm.expr().clone());
461        let fetch = spm.fetch();
462        Ok((
463            Arc::new(OutputRequirementExec::new(
464                plan,
465                Some(reqs),
466                Distribution::SinglePartition,
467                fetch,
468            )) as _,
469            true,
470        ))
471    } else if let Some(idx) = output_requirement_child(plan.as_ref()) {
472        // Keep searching for a `SortExec` / `SortPreservingMergeExec` as long as
473        // ordering is maintained, and on-the-way operators do not themselves
474        // require an ordering. When an operator requires an ordering, any
475        // `SortExec` below can not be responsible for (i.e. the originator of)
476        // the global ordering.
477        if plan.maintains_input_order()[idx]
478            && plan.required_input_ordering()[idx]
479                .as_ref()
480                .is_none_or(|o| matches!(o, OrderingRequirements::Soft(_)))
481        {
482            let mut children: Vec<Arc<dyn ExecutionPlan>> =
483                plan.children().into_iter().map(Arc::clone).collect();
484            let (new_child, is_changed) =
485                require_top_ordering_helper(Arc::clone(&children[idx]))?;
486            if is_changed {
487                children[idx] = new_child;
488                return Ok((replace_children_if_necessary(plan, children)?, true));
489            }
490        }
491        Ok((plan, false))
492    } else {
493        // Stop searching, there is no global ordering desired for the query.
494        Ok((plan, false))
495    }
496}
497
498// See tests in datafusion/core/tests/physical_optimizer