Skip to main content

datafusion_physical_plan/windows/
window_agg_exec.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//! Stream and channel implementations for window function expressions.
19
20use std::pin::Pin;
21use std::sync::Arc;
22use std::task::{Context, Poll};
23
24#[cfg(feature = "proto")]
25use super::proto::{decode_physical_window_expr, encode_physical_window_expr};
26use super::utils::create_schema;
27use crate::execution_plan::{CardinalityEffect, EmissionType};
28use crate::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
29use crate::statistics::{ChildStats, StatisticsArgs};
30use crate::stream::EmptyRecordBatchStream;
31use crate::windows::{
32    calc_requirements, get_ordered_partition_by_indices, get_partition_by_sort_exprs,
33    window_equivalence_properties,
34};
35use crate::{
36    ChildrenPropertiesMode, ColumnStatistics, DisplayAs, DisplayFormatType, Distribution,
37    ExecutionPlan, ExecutionPlanProperties, InputDistributionRequirements, PhysicalExpr,
38    PlanProperties, RecordBatchStream, ReplaceChildrenOptions, SendableRecordBatchStream,
39    Statistics, WindowExpr, validate_child_count,
40};
41
42use arrow::array::ArrayRef;
43use arrow::compute::{concat, concat_batches};
44use arrow::datatypes::SchemaRef;
45use arrow::error::ArrowError;
46use arrow::record_batch::RecordBatch;
47use datafusion_common::stats::Precision;
48use datafusion_common::tree_node::TreeNodeRecursion;
49use datafusion_common::utils::{evaluate_partition_ranges, transpose};
50use datafusion_common::{Result, assert_eq_or_internal_err};
51use datafusion_execution::TaskContext;
52use datafusion_physical_expr_common::sort_expr::{
53    OrderingRequirements, PhysicalSortExpr,
54};
55
56use futures::{Stream, StreamExt, ready};
57
58/// Window execution plan
59#[derive(Debug, Clone)]
60pub struct WindowAggExec {
61    /// Input plan
62    pub(crate) input: Arc<dyn ExecutionPlan>,
63    /// Window function expression
64    window_expr: Vec<Arc<dyn WindowExpr>>,
65    /// Schema after the window is run
66    schema: SchemaRef,
67    /// Execution metrics
68    metrics: ExecutionPlanMetricsSet,
69    /// Partition by indices that defines preset for existing ordering
70    // see `get_ordered_partition_by_indices` for more details.
71    ordered_partition_by_indices: Vec<usize>,
72    /// Cache holding plan properties like equivalences, output partitioning etc.
73    cache: Arc<PlanProperties>,
74    /// If `can_partition` is false, partition_keys is always empty.
75    can_repartition: bool,
76}
77
78impl WindowAggExec {
79    /// Create a new execution plan for window aggregates
80    pub fn try_new(
81        window_expr: Vec<Arc<dyn WindowExpr>>,
82        input: Arc<dyn ExecutionPlan>,
83        can_repartition: bool,
84    ) -> Result<Self> {
85        let schema = create_schema(&input.schema(), &window_expr)?;
86        let schema = Arc::new(schema);
87
88        let ordered_partition_by_indices =
89            get_ordered_partition_by_indices(window_expr[0].partition_by(), &input)?;
90        let cache = Self::compute_properties(&schema, &input, &window_expr)?;
91        Ok(Self {
92            input,
93            window_expr,
94            schema,
95            metrics: ExecutionPlanMetricsSet::new(),
96            ordered_partition_by_indices,
97            cache: Arc::new(cache),
98            can_repartition,
99        })
100    }
101
102    /// Window expressions
103    pub fn window_expr(&self) -> &[Arc<dyn WindowExpr>] {
104        &self.window_expr
105    }
106
107    /// Input plan
108    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
109        &self.input
110    }
111
112    /// Return the output sort order of partition keys: For example
113    /// OVER(PARTITION BY a, ORDER BY b) -> would give sorting of the column a
114    // We are sure that partition by columns are always at the beginning of sort_keys
115    // Hence returned `PhysicalSortExpr` corresponding to `PARTITION BY` columns can be used safely
116    // to calculate partition separation points
117    pub fn partition_by_sort_keys(&self) -> Result<Vec<PhysicalSortExpr>> {
118        let partition_by = self.window_expr()[0].partition_by();
119        get_partition_by_sort_exprs(
120            &self.input,
121            partition_by,
122            &self.ordered_partition_by_indices,
123        )
124    }
125
126    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
127    fn compute_properties(
128        schema: &SchemaRef,
129        input: &Arc<dyn ExecutionPlan>,
130        window_exprs: &[Arc<dyn WindowExpr>],
131    ) -> Result<PlanProperties> {
132        // Calculate equivalence properties:
133        let eq_properties = window_equivalence_properties(schema, input, window_exprs)?;
134
135        // Get output partitioning:
136        // Because we can have repartitioning using the partition keys this
137        // would be either 1 or more than 1 depending on the presence of repartitioning.
138        let output_partitioning = input.output_partitioning().clone();
139
140        // Construct properties cache:
141        Ok(PlanProperties::new(
142            eq_properties,
143            output_partitioning,
144            // TODO: Emission type and boundedness information can be enhanced here
145            EmissionType::Final,
146            input.boundedness(),
147        ))
148    }
149
150    pub fn partition_keys(&self) -> Vec<Arc<dyn PhysicalExpr>> {
151        if !self.can_repartition {
152            vec![]
153        } else {
154            let all_partition_keys = self
155                .window_expr()
156                .iter()
157                .map(|expr| expr.partition_by().to_vec())
158                .collect::<Vec<_>>();
159
160            all_partition_keys
161                .into_iter()
162                .min_by_key(|s| s.len())
163                .unwrap_or_else(Vec::new)
164        }
165    }
166}
167
168impl DisplayAs for WindowAggExec {
169    fn fmt_as(
170        &self,
171        t: DisplayFormatType,
172        f: &mut std::fmt::Formatter,
173    ) -> std::fmt::Result {
174        match t {
175            DisplayFormatType::Default | DisplayFormatType::Verbose => {
176                write!(f, "WindowAggExec: ")?;
177                let g: Vec<String> = self
178                    .window_expr
179                    .iter()
180                    .map(|e| {
181                        format!(
182                            "{}: {:?}, frame: {:?}",
183                            e.name().to_owned(),
184                            e.field(),
185                            e.get_window_frame()
186                        )
187                    })
188                    .collect();
189                write!(f, "wdw=[{}]", g.join(", "))?;
190            }
191            DisplayFormatType::TreeRender => {
192                let g: Vec<String> = self
193                    .window_expr
194                    .iter()
195                    .map(|e| e.name().to_owned().to_string())
196                    .collect();
197                writeln!(f, "select_list={}", g.join(", "))?;
198            }
199        }
200        Ok(())
201    }
202}
203
204impl ExecutionPlan for WindowAggExec {
205    fn name(&self) -> &'static str {
206        "WindowAggExec"
207    }
208
209    /// Return a reference to Any that can be used for downcasting
210    fn properties(&self) -> &Arc<PlanProperties> {
211        &self.cache
212    }
213
214    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
215        vec![&self.input]
216    }
217
218    fn apply_expressions(
219        &self,
220        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
221    ) -> Result<TreeNodeRecursion> {
222        let expressions = self.window_expr.iter().flat_map(|window_expr| {
223            let expressions = window_expr.all_expressions();
224            expressions
225                .args
226                .into_iter()
227                .chain(expressions.partition_by_exprs)
228                .chain(expressions.order_by_exprs)
229        });
230        crate::apply_expression_roots(expressions, f)
231    }
232
233    fn maintains_input_order(&self) -> Vec<bool> {
234        vec![true]
235    }
236
237    fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
238        let partition_bys = self.window_expr()[0].partition_by();
239        let order_keys = self.window_expr()[0].order_by();
240        if self.ordered_partition_by_indices.len() < partition_bys.len() {
241            vec![calc_requirements(partition_bys, order_keys)]
242        } else {
243            let partition_bys = self
244                .ordered_partition_by_indices
245                .iter()
246                .map(|idx| &partition_bys[*idx]);
247            vec![calc_requirements(partition_bys, order_keys)]
248        }
249    }
250
251    fn required_input_distribution(&self) -> Vec<Distribution> {
252        self.input_distribution_requirements().into_per_child()
253    }
254
255    fn input_distribution_requirements(&self) -> InputDistributionRequirements {
256        if self.partition_keys().is_empty() {
257            InputDistributionRequirements::new(vec![Distribution::SinglePartition])
258        } else {
259            InputDistributionRequirements::new(vec![Distribution::KeyPartitioned(
260                self.partition_keys(),
261            )])
262        }
263    }
264
265    fn replace_children(
266        self: Arc<Self>,
267        mut children: Vec<Arc<dyn ExecutionPlan>>,
268        options: ReplaceChildrenOptions,
269    ) -> Result<Arc<dyn ExecutionPlan>> {
270        validate_child_count!(self, children);
271        match options.children_properties {
272            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
273                input: children.swap_remove(0),
274                metrics: ExecutionPlanMetricsSet::new(),
275                ..Self::clone(&*self)
276            })),
277            ChildrenPropertiesMode::Recompute => Ok(Arc::new(WindowAggExec::try_new(
278                self.window_expr.clone(),
279                children.swap_remove(0),
280                true,
281            )?)),
282        }
283    }
284
285    fn with_new_children(
286        self: Arc<Self>,
287        children: Vec<Arc<dyn ExecutionPlan>>,
288    ) -> Result<Arc<dyn ExecutionPlan>> {
289        self.replace_children(
290            children,
291            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
292        )
293    }
294
295    fn with_new_children_and_same_properties(
296        self: Arc<Self>,
297        children: Vec<Arc<dyn ExecutionPlan>>,
298    ) -> Result<Arc<dyn ExecutionPlan>> {
299        self.replace_children(
300            children,
301            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
302        )
303    }
304
305    fn execute(
306        &self,
307        partition: usize,
308        context: Arc<TaskContext>,
309    ) -> Result<SendableRecordBatchStream> {
310        let input = self.input.execute(partition, context)?;
311        let stream = Box::pin(WindowAggStream::new(
312            Arc::clone(&self.schema),
313            self.window_expr.clone(),
314            input,
315            BaselineMetrics::new(&self.metrics, partition),
316            self.partition_by_sort_keys()?,
317            self.ordered_partition_by_indices.clone(),
318        )?);
319        Ok(stream)
320    }
321
322    fn metrics(&self) -> Option<MetricsSet> {
323        Some(self.metrics.clone_inner())
324    }
325
326    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
327        vec![ChildStats::At(partition)]
328    }
329
330    fn statistics_from_inputs(
331        &self,
332        input_stats: &[Arc<Statistics>],
333        _args: &StatisticsArgs,
334    ) -> Result<Arc<Statistics>> {
335        let input_stat = input_stats[0].as_ref().clone();
336        let win_cols = self.window_expr.len();
337        let input_cols = self.input.schema().fields().len();
338        // TODO stats: some windowing function will maintain invariants such as min, max...
339        let mut column_statistics = Vec::with_capacity(win_cols + input_cols);
340        // copy stats of the input to the beginning of the schema.
341        column_statistics.extend(input_stat.column_statistics);
342        for _ in 0..win_cols {
343            column_statistics.push(ColumnStatistics::new_unknown())
344        }
345        Ok(Arc::new(Statistics {
346            num_rows: input_stat.num_rows,
347            column_statistics,
348            total_byte_size: Precision::Absent,
349        }))
350    }
351
352    fn cardinality_effect(&self) -> CardinalityEffect {
353        CardinalityEffect::Equal
354    }
355
356    #[cfg(feature = "proto")]
357    fn try_to_proto(
358        &self,
359        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
360    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
361        use datafusion_proto_models::protobuf;
362
363        // Exhaustive destructure: adding a field to `WindowAggExec` without
364        // deciding how it is serialized is a compile error, not a silent
365        // round-trip gap.
366        let Self {
367            input,
368            window_expr,
369            // Derived at construction by `create_schema` from the input schema
370            // and the window expressions.
371            schema: _,
372            // Runtime execution state, rebuilt empty on decode.
373            metrics: _,
374            // Derived at construction by `get_ordered_partition_by_indices`.
375            ordered_partition_by_indices: _,
376            // Derived at construction by `Self::compute_properties`.
377            cache: _,
378            // No wire field of its own; it is folded into `partition_keys`
379            // below, since `partition_keys()` returns an empty vec when this is
380            // false and the decoder recovers it as `!partition_keys.is_empty()`.
381            can_repartition: _,
382        } = self;
383
384        let input = ctx.encode_child(input)?;
385        let window_expr = window_expr
386            .iter()
387            .map(|expr| encode_physical_window_expr(expr, ctx))
388            .collect::<Result<Vec<_>>>()?;
389        let partition_keys = self
390            .partition_keys()
391            .iter()
392            .map(|expr| ctx.encode_expr(expr))
393            .collect::<Result<Vec<_>>>()?;
394
395        Ok(Some(protobuf::PhysicalPlanNode {
396            physical_plan_type: Some(
397                protobuf::physical_plan_node::PhysicalPlanType::Window(Box::new(
398                    protobuf::WindowAggExecNode {
399                        input: Some(Box::new(input)),
400                        window_expr,
401                        partition_keys,
402                        // `None` distinguishes a `WindowAggExec` from a
403                        // `BoundedWindowAggExec` on the shared `Window` variant.
404                        input_order_mode: None,
405                    },
406                )),
407            ),
408        }))
409    }
410}
411
412#[cfg(feature = "proto")]
413impl WindowAggExec {
414    /// Reconstruct a window plan from its protobuf representation.
415    ///
416    /// This returns a [`WindowAggExec`] when `input_order_mode` is absent and a
417    /// [`BoundedWindowAggExec`] when it is present.
418    ///
419    /// [`BoundedWindowAggExec`]: crate::windows::BoundedWindowAggExec
420    pub fn try_from_proto(
421        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
422        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
423    ) -> Result<Arc<dyn ExecutionPlan>> {
424        use super::BoundedWindowAggExec;
425        use crate::InputOrderMode;
426        use datafusion_proto_models::protobuf;
427        use protobuf::window_agg_exec_node::InputOrderMode as ProtoInputOrderMode;
428
429        let window_agg = crate::expect_plan_variant!(
430            node,
431            protobuf::physical_plan_node::PhysicalPlanType::Window,
432            "WindowAggExec",
433        );
434        // Exhaustive destructure: a new field on `WindowAggExecNode` is a
435        // compile error here rather than a silently ignored wire field.
436        let protobuf::WindowAggExecNode {
437            input,
438            window_expr,
439            partition_keys,
440            input_order_mode,
441        } = window_agg.as_ref();
442
443        let input =
444            ctx.decode_required_child(input.as_deref(), "WindowAggExec", "input")?;
445        let input_schema = input.schema();
446        let window_expr = window_expr
447            .iter()
448            .map(|expr| decode_physical_window_expr(expr, ctx, input_schema.as_ref()))
449            .collect::<Result<Vec<_>>>()?;
450        let partition_keys = partition_keys
451            .iter()
452            .map(|expr| ctx.decode_expr(expr, input_schema.as_ref()))
453            .collect::<Result<Vec<_>>>()?;
454
455        if let Some(input_order_mode) = input_order_mode.as_ref() {
456            let input_order_mode = match input_order_mode {
457                ProtoInputOrderMode::Linear(_) => InputOrderMode::Linear,
458                ProtoInputOrderMode::PartiallySorted(
459                    protobuf::PartiallySortedInputOrderMode { columns },
460                ) => InputOrderMode::PartiallySorted(
461                    columns.iter().map(|column| *column as usize).collect(),
462                ),
463                ProtoInputOrderMode::Sorted(_) => InputOrderMode::Sorted,
464            };
465            Ok(Arc::new(BoundedWindowAggExec::try_new(
466                window_expr,
467                input,
468                input_order_mode,
469                // `can_repartition` has no wire field: the encoder writes an
470                // empty `partition_keys` when it is false.
471                !partition_keys.is_empty(),
472            )?))
473        } else {
474            Ok(Arc::new(WindowAggExec::try_new(
475                window_expr,
476                input,
477                // See above: `can_repartition` is recovered from `partition_keys`.
478                !partition_keys.is_empty(),
479            )?))
480        }
481    }
482}
483
484/// Compute the window aggregate columns
485fn compute_window_aggregates(
486    window_expr: &[Arc<dyn WindowExpr>],
487    batch: &RecordBatch,
488) -> Result<Vec<ArrayRef>> {
489    window_expr
490        .iter()
491        .map(|window_expr| window_expr.evaluate(batch))
492        .collect()
493}
494
495/// stream for window aggregation plan
496pub struct WindowAggStream {
497    schema: SchemaRef,
498    input: SendableRecordBatchStream,
499    batches: Vec<RecordBatch>,
500    finished: bool,
501    window_expr: Vec<Arc<dyn WindowExpr>>,
502    partition_by_sort_keys: Vec<PhysicalSortExpr>,
503    baseline_metrics: BaselineMetrics,
504    ordered_partition_by_indices: Vec<usize>,
505}
506
507impl WindowAggStream {
508    /// Create a new WindowAggStream
509    pub fn new(
510        schema: SchemaRef,
511        window_expr: Vec<Arc<dyn WindowExpr>>,
512        input: SendableRecordBatchStream,
513        baseline_metrics: BaselineMetrics,
514        partition_by_sort_keys: Vec<PhysicalSortExpr>,
515        ordered_partition_by_indices: Vec<usize>,
516    ) -> Result<Self> {
517        // In WindowAggExec all partition by columns should be ordered.
518        assert_eq_or_internal_err!(
519            window_expr[0].partition_by().len(),
520            ordered_partition_by_indices.len(),
521            "All partition by columns should have an ordering"
522        );
523        Ok(Self {
524            schema,
525            input,
526            batches: vec![],
527            finished: false,
528            window_expr,
529            baseline_metrics,
530            partition_by_sort_keys,
531            ordered_partition_by_indices,
532        })
533    }
534
535    fn compute_aggregates(&self) -> Result<Option<RecordBatch>> {
536        // record compute time on drop
537        let _timer = self.baseline_metrics.elapsed_compute().timer();
538
539        let batch = concat_batches(&self.input.schema(), &self.batches)?;
540        if batch.num_rows() == 0 {
541            return Ok(None);
542        }
543
544        let partition_by_sort_keys = self
545            .ordered_partition_by_indices
546            .iter()
547            .map(|idx| self.partition_by_sort_keys[*idx].evaluate_to_sort_column(&batch))
548            .collect::<Result<Vec<_>>>()?;
549        let partition_points =
550            evaluate_partition_ranges(batch.num_rows(), &partition_by_sort_keys)?;
551
552        let mut partition_results = vec![];
553        // Calculate window cols
554        for partition_point in partition_points {
555            let length = partition_point.end - partition_point.start;
556            partition_results.push(compute_window_aggregates(
557                &self.window_expr,
558                &batch.slice(partition_point.start, length),
559            )?)
560        }
561        let columns = transpose(partition_results)
562            .iter()
563            .map(|elems| concat(&elems.iter().map(|x| x.as_ref()).collect::<Vec<_>>()))
564            .collect::<Vec<_>>()
565            .into_iter()
566            .collect::<Result<Vec<ArrayRef>, ArrowError>>()?;
567
568        // combine with the original cols
569        // note the setup of window aggregates is that they newly calculated window
570        // expression results are always appended to the columns
571        let mut batch_columns = batch.columns().to_vec();
572        // calculate window cols
573        batch_columns.extend_from_slice(&columns);
574        Ok(Some(RecordBatch::try_new(
575            Arc::clone(&self.schema),
576            batch_columns,
577        )?))
578    }
579}
580
581impl Stream for WindowAggStream {
582    type Item = Result<RecordBatch>;
583
584    fn poll_next(
585        mut self: Pin<&mut Self>,
586        cx: &mut Context<'_>,
587    ) -> Poll<Option<Self::Item>> {
588        let poll = self.poll_next_inner(cx);
589        self.baseline_metrics.record_poll(poll)
590    }
591}
592
593impl WindowAggStream {
594    #[inline]
595    fn poll_next_inner(
596        &mut self,
597        cx: &mut Context<'_>,
598    ) -> Poll<Option<Result<RecordBatch>>> {
599        if self.finished {
600            return Poll::Ready(None);
601        }
602
603        loop {
604            return Poll::Ready(Some(match ready!(self.input.poll_next_unpin(cx)) {
605                Some(Ok(batch)) => {
606                    self.batches.push(batch);
607                    continue;
608                }
609                Some(Err(e)) => Err(e),
610                None => {
611                    // Release the input pipeline's resources before computing
612                    // the final aggregates.
613                    let input_schema = self.input.schema();
614                    self.input = Box::pin(EmptyRecordBatchStream::new(input_schema));
615                    let Some(result) = self.compute_aggregates()? else {
616                        return Poll::Ready(None);
617                    };
618                    self.finished = true;
619                    // Empty record batches should not be emitted.
620                    // They need to be treated as  [`Option<RecordBatch>`]es and handled separately
621                    debug_assert!(result.num_rows() > 0);
622                    Ok(result)
623                }
624            }));
625        }
626    }
627}
628
629impl RecordBatchStream for WindowAggStream {
630    /// Get the schema
631    fn schema(&self) -> SchemaRef {
632        Arc::clone(&self.schema)
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use crate::test::TestMemoryExec;
640    use crate::windows::create_window_expr;
641    use arrow::datatypes::{DataType, Field, Schema};
642    use datafusion_common::ScalarValue;
643    use datafusion_expr::{
644        WindowFrame, WindowFrameBound, WindowFrameUnits, WindowFunctionDefinition,
645    };
646    use datafusion_functions_aggregate::count::count_udaf;
647
648    #[test]
649    fn test_window_agg_cardinality_effect() -> Result<()> {
650        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, true)]));
651        let input: Arc<dyn ExecutionPlan> =
652            Arc::new(TestMemoryExec::try_new(&[], Arc::clone(&schema), None)?);
653        let args = vec![crate::expressions::col("a", &schema)?];
654        let window_expr = create_window_expr(
655            &WindowFunctionDefinition::AggregateUDF(count_udaf()),
656            "count(a)".to_string(),
657            &args,
658            &[],
659            &[],
660            Arc::new(WindowFrame::new_bounds(
661                WindowFrameUnits::Rows,
662                WindowFrameBound::Preceding(ScalarValue::UInt64(None)),
663                WindowFrameBound::CurrentRow,
664            )),
665            Arc::clone(&schema),
666            false,
667            false,
668            None,
669        )?;
670
671        let window = WindowAggExec::try_new(vec![window_expr], input, true)?;
672        assert!(matches!(
673            window.cardinality_effect(),
674            CardinalityEffect::Equal
675        ));
676        Ok(())
677    }
678}