Skip to main content

datafusion_physical_plan/sorts/
partitioned_topk.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//! [`PartitionedTopKExec`]: Top-K per partition operator
19//!
20//! For queries like:
21//! ```sql
22//! SELECT *, ROW_NUMBER() OVER (PARTITION BY pk ORDER BY val) as rn
23//! FROM t WHERE rn <= N
24//! ```
25//!
26//! Instead of sorting the entire dataset, this operator delegates to a
27//! per-partition heap-of-K implementation (one variant for `ROW_NUMBER`
28//! and a sibling variant for `RANK`), both of which maintain one heap per
29//! distinct partition key while sharing a single [`arrow::row::RowConverter`],
30//! [`MemoryReservation`](datafusion_execution::memory_pool::MemoryReservation),
31//! and metrics set across all partitions, and emit only the top-K rows
32//! per partition in sorted order `(partition_keys, order_keys)`.
33
34use std::fmt::{self, Formatter};
35use std::sync::Arc;
36
37use arrow::datatypes::SchemaRef;
38use arrow::row::SortField;
39use datafusion_common::Result;
40use datafusion_common::tree_node::TreeNodeRecursion;
41use datafusion_execution::TaskContext;
42use datafusion_execution::runtime_env::RuntimeEnv;
43use datafusion_physical_expr::PhysicalExpr;
44use datafusion_physical_expr_common::sort_expr::LexOrdering;
45use futures::StreamExt;
46use futures::TryStreamExt;
47
48use crate::execution_plan::{Boundedness, EmissionType};
49use crate::metrics::ExecutionPlanMetricsSet;
50use crate::topk::{PartitionedTopK, PartitionedTopKRank, build_sort_fields};
51use crate::{ChildrenPropertiesMode, ReplaceChildrenOptions};
52use crate::{
53    DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, ExecutionPlanProperties,
54    PlanProperties, SendableRecordBatchStream, stream::RecordBatchStreamAdapter,
55};
56
57/// Which window function `PartitionedTopKExec` is optimizing.
58///
59/// Different ranking functions have different per-partition retention rules:
60/// - [`RowNumber`](Self::RowNumber): exactly K rows per partition.
61/// - [`Rank`](Self::Rank): K rows plus any rows tied at the boundary
62///   ORDER BY value (RANK semantics — `WHERE rk <= K` may keep more
63///   than K rows when ties straddle the boundary).
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum WindowFnKind {
66    /// `ROW_NUMBER()` — keep exactly K rows per partition.
67    RowNumber,
68    /// `RANK()` — keep K rows plus any rows tied at the boundary.
69    Rank,
70}
71
72/// Per-partition Top-K operator for window function queries.
73///
74/// # Background
75///
76/// "Top K per partition" is a common analytics pattern used for queries such as
77/// "find the top 3 products by revenue for each store". The (simplified) SQL
78/// for such a query might be:
79///
80/// ```sql
81/// SELECT * FROM (
82///     SELECT *, ROW_NUMBER() OVER (PARTITION BY store ORDER BY revenue DESC) as rn
83///     FROM sales
84/// ) WHERE rn <= 3;
85/// ```
86///
87/// The unoptimized physical plan would be:
88///
89/// ```text
90/// FilterExec: rn <= 3
91///   BoundedWindowAggExec: ROW_NUMBER() PARTITION BY [store] ORDER BY [revenue DESC]
92///     SortExec: expr=[store ASC, revenue DESC]
93///       DataSourceExec
94/// ```
95///
96/// This plan sorts the **entire** dataset (O(N log N)), computes `ROW_NUMBER`
97/// for **all** rows, and then filters to keep only the top K per partition.
98/// With 10M rows, 1K partitions, and K=3, it sorts all 10M rows but only
99/// keeps 3K.
100///
101/// # Optimization
102///
103/// `PartitionedTopKExec` replaces the `SortExec` and the `FilterExec` is
104/// removed. The optimized plan becomes:
105///
106/// ```text
107/// BoundedWindowAggExec: ROW_NUMBER() PARTITION BY [store] ORDER BY [revenue DESC]
108///   PartitionedTopKExec: fetch=3, partition=[store], order=[revenue DESC]
109///     DataSourceExec
110/// ```
111///
112/// Instead of sorting the entire dataset, this operator reads unsorted input
113/// and delegates to a per-partition heap-of-K implementation (`PartitionedTopK`
114/// for `ROW_NUMBER` and `PartitionedTopKRank` for `RANK`), each maintaining
115/// one heap per distinct partition key while sharing a single
116/// [`arrow::row::RowConverter`] /
117/// [`MemoryReservation`](datafusion_execution::memory_pool::MemoryReservation)
118/// across all partitions, and emits only the top-K rows per partition in
119/// sorted order `(partition_keys, order_keys)`.
120///
121/// Cost: O(N log K) time instead of O(N log N), and O(K × P × row_size)
122/// memory where K = fetch, P = number of distinct partitions.
123/// ## Why maintaining partition key order in output
124/// Window functions do not require partition keys to be globally sorted, and
125/// enforcing such ordering in the output can introduce unnecessary overhead.
126/// However, the physical optimizer framework currently cannot express an
127/// ordering that is only grouped by some keys while ordered by others. For
128/// example:
129///
130///
131/// # Example
132///
133/// For the query above with `fetch=3` and input:
134///
135/// ```text
136/// store | revenue
137/// ------|--------
138///   A   |  100
139///   B   |   50
140///   A   |  200
141///   B   |  150
142///   A   |  300
143///   A   |  400
144/// ```
145///
146/// The operator maintains two heaps:
147/// - **store=A**: keeps top-3 by revenue DESC → {400, 300, 200}, evicts 100
148/// - **store=B**: keeps top-3 by revenue DESC → {150, 50} (only 2 rows)
149///
150/// Output (sorted by store ASC, revenue DESC):
151///
152/// ```text
153/// store | revenue
154/// ------|--------
155///   A   |  400
156///   A   |  300
157///   A   |  200
158///   B   |  150
159///   B   |   50
160/// ```
161///
162/// This is then passed to `BoundedWindowAggExec` which assigns
163/// `ROW_NUMBER` 1, 2, 3 to each partition — all of which satisfy `rn <= 3`.
164///
165/// # Limitations
166///
167/// - Only activated when the window function is `ROW_NUMBER` or `RANK` with
168///   a `PARTITION BY` clause. `RANK` additionally requires a non-empty
169///   `ORDER BY` (with an empty `ORDER BY`, every row ties at rank 1 and the
170///   heap-of-K rewrite doesn't apply). Global top-K (no `PARTITION BY`) is
171///   already handled efficiently by `SortExec` with `fetch`.
172/// - For very high cardinality partition keys (millions of distinct values),
173///   both memory usage and runtime overhead can become significant. In such
174///   cases, the sort-based plan is more robust. Therefore, this optimization
175///   is currently controlled by a configuration flag.
176#[derive(Debug, Clone)]
177pub struct PartitionedTopKExec {
178    /// Input execution plan (reads unsorted data)
179    input: Arc<dyn ExecutionPlan>,
180    /// Full sort expressions: `[partition_keys..., order_keys...]`.
181    ///
182    /// For `PARTITION BY store ORDER BY revenue DESC` with sort
183    /// `[store ASC, revenue DESC]`, the first `partition_prefix_len`
184    /// expressions are the partition keys (`[store ASC]`) and the
185    /// remaining are the order-by keys (`[revenue DESC]`).
186    expr: LexOrdering,
187    /// Number of leading expressions in `expr` that define the partition
188    /// key. For example, `PARTITION BY a, b` → `partition_prefix_len = 2`.
189    partition_prefix_len: usize,
190    /// Maximum number of rows to keep per partition (the K in "top-K").
191    /// Derived from the filter predicate: `rn <= 3` → `fetch = 3`,
192    /// `rn < 3` → `fetch = 2`.
193    fetch: usize,
194    /// Which window function this operator is optimizing. Selects the
195    /// per-partition retention policy (see [`WindowFnKind`]).
196    fn_kind: WindowFnKind,
197    /// Execution metrics
198    metrics_set: ExecutionPlanMetricsSet,
199    /// Cached plan properties (output ordering, partitioning, etc.)
200    cache: Arc<PlanProperties>,
201}
202
203impl PartitionedTopKExec {
204    /// Create a new `PartitionedTopKExec`.
205    ///
206    /// # Arguments
207    ///
208    /// * `input` - The child execution plan providing unsorted input rows.
209    /// * `expr` - Full sort ordering `[partition_keys..., order_keys...]`.
210    ///   For `PARTITION BY pk ORDER BY val ASC`, this would be `[pk ASC, val ASC]`.
211    /// * `partition_prefix_len` - Number of leading expressions in `expr`
212    ///   that form the partition key. Must be >= 1.
213    /// * `fetch` - Maximum rows to retain per partition (the K in "top-K").
214    /// * `fn_kind` - Which ranking window function this operator optimizes
215    ///   ([`WindowFnKind::RowNumber`] or [`WindowFnKind::Rank`]).
216    ///
217    /// # Example
218    ///
219    /// ```text
220    /// // For: ROW_NUMBER() OVER (PARTITION BY store ORDER BY revenue DESC) ... WHERE rn <= 5
221    /// PartitionedTopKExec::try_new(
222    ///     data_source,
223    ///     LexOrdering([store ASC, revenue DESC]),
224    ///     1,    // partition_prefix_len: 1 partition column (store)
225    ///     5,    // fetch: keep top 5 per partition
226    ///     WindowFnKind::RowNumber,
227    /// )
228    /// ```
229    pub fn try_new(
230        input: Arc<dyn ExecutionPlan>,
231        expr: LexOrdering,
232        partition_prefix_len: usize,
233        fetch: usize,
234        fn_kind: WindowFnKind,
235    ) -> Result<Self> {
236        let cache = Self::compute_properties(&input, expr.clone())?;
237        Ok(Self {
238            input,
239            expr,
240            partition_prefix_len,
241            fetch,
242            fn_kind,
243            metrics_set: ExecutionPlanMetricsSet::new(),
244            cache: Arc::new(cache),
245        })
246    }
247
248    /// Returns the child execution plan.
249    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
250        &self.input
251    }
252
253    /// Returns the full sort ordering `[partition_keys..., order_keys...]`.
254    pub fn expr(&self) -> &LexOrdering {
255        &self.expr
256    }
257
258    /// Returns the number of leading expressions in [`Self::expr`] that
259    /// define the partition key.
260    pub fn partition_prefix_len(&self) -> usize {
261        self.partition_prefix_len
262    }
263
264    /// Returns the maximum number of rows retained per partition.
265    pub fn fetch(&self) -> usize {
266        self.fetch
267    }
268
269    /// Returns which window function this operator is optimizing.
270    pub fn fn_kind(&self) -> WindowFnKind {
271        self.fn_kind
272    }
273
274    /// Compute [`PlanProperties`] for this operator.
275    ///
276    /// The output is sorted by `sort_exprs` (partition keys then order keys),
277    /// uses the same partitioning as the input, emits all output at once
278    /// (`EmissionType::Final`), and is bounded.
279    fn compute_properties(
280        input: &Arc<dyn ExecutionPlan>,
281        sort_exprs: LexOrdering,
282    ) -> Result<PlanProperties> {
283        let mut eq_properties = input.equivalence_properties().clone();
284        eq_properties.reorder(sort_exprs)?;
285
286        Ok(PlanProperties::new(
287            eq_properties,
288            input.output_partitioning().clone(),
289            EmissionType::Final,
290            Boundedness::Bounded,
291        ))
292    }
293}
294
295impl DisplayAs for PartitionedTopKExec {
296    fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result {
297        let fn_label = match self.fn_kind {
298            WindowFnKind::RowNumber => "row_number",
299            WindowFnKind::Rank => "rank",
300        };
301        match t {
302            DisplayFormatType::Default | DisplayFormatType::Verbose => {
303                let partition_exprs: Vec<String> = self.expr[..self.partition_prefix_len]
304                    .iter()
305                    .map(|e| format!("{}", e.expr))
306                    .collect();
307                let order_exprs: Vec<String> = self.expr[self.partition_prefix_len..]
308                    .iter()
309                    .map(|e| format!("{e}"))
310                    .collect();
311                write!(
312                    f,
313                    "PartitionedTopKExec: fn={}, fetch={}, partition=[{}], order=[{}]",
314                    fn_label,
315                    self.fetch,
316                    partition_exprs.join(", "),
317                    order_exprs.join(", "),
318                )
319            }
320            DisplayFormatType::TreeRender => {
321                let partition_exprs: Vec<String> = self.expr[..self.partition_prefix_len]
322                    .iter()
323                    .map(|e| format!("{}", e.expr))
324                    .collect();
325                let order_exprs: Vec<String> = self.expr[self.partition_prefix_len..]
326                    .iter()
327                    .map(|e| format!("{e}"))
328                    .collect();
329                writeln!(f, "fn={fn_label}")?;
330                writeln!(f, "fetch={}", self.fetch)?;
331                writeln!(f, "partition=[{}]", partition_exprs.join(", "))?;
332                writeln!(f, "order=[{}]", order_exprs.join(", "))
333            }
334        }
335    }
336}
337
338impl ExecutionPlan for PartitionedTopKExec {
339    fn name(&self) -> &'static str {
340        "PartitionedTopKExec"
341    }
342
343    fn properties(&self) -> &Arc<PlanProperties> {
344        &self.cache
345    }
346
347    fn required_input_distribution(&self) -> Vec<Distribution> {
348        self.input_distribution_requirements().into_per_child()
349    }
350
351    fn input_distribution_requirements(&self) -> crate::InputDistributionRequirements {
352        let partition_exprs: Vec<Arc<dyn PhysicalExpr>> = self.expr
353            [..self.partition_prefix_len]
354            .iter()
355            .map(|e| Arc::clone(&e.expr))
356            .collect();
357        crate::InputDistributionRequirements::new(vec![Distribution::KeyPartitioned(
358            partition_exprs,
359        )])
360    }
361
362    fn maintains_input_order(&self) -> Vec<bool> {
363        vec![false]
364    }
365
366    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
367        vec![&self.input]
368    }
369
370    fn replace_children(
371        self: Arc<Self>,
372        children: Vec<Arc<dyn ExecutionPlan>>,
373        _: ReplaceChildrenOptions,
374    ) -> Result<Arc<dyn ExecutionPlan>> {
375        assert_eq!(children.len(), 1);
376        Ok(Arc::new(PartitionedTopKExec::try_new(
377            Arc::clone(&children[0]),
378            self.expr.clone(),
379            self.partition_prefix_len,
380            self.fetch,
381            self.fn_kind,
382        )?))
383    }
384
385    fn apply_expressions(
386        &self,
387        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
388    ) -> Result<TreeNodeRecursion> {
389        crate::apply_expression_roots(
390            self.expr.iter().map(|sort_expr| &sort_expr.expr),
391            f,
392        )
393    }
394
395    fn with_new_children(
396        self: Arc<Self>,
397        children: Vec<Arc<dyn ExecutionPlan>>,
398    ) -> Result<Arc<dyn ExecutionPlan>> {
399        self.replace_children(
400            children,
401            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
402        )
403    }
404
405    fn execute(
406        &self,
407        partition: usize,
408        context: Arc<TaskContext>,
409    ) -> Result<SendableRecordBatchStream> {
410        let input = self.input.execute(partition, Arc::clone(&context))?;
411        let schema = input.schema();
412
413        let partition_sort_fields =
414            build_sort_fields(&self.expr[..self.partition_prefix_len], &schema)?;
415
416        let partition_exprs: Vec<Arc<dyn PhysicalExpr>> = self.expr
417            [..self.partition_prefix_len]
418            .iter()
419            .map(|e| Arc::clone(&e.expr))
420            .collect();
421        let order_expr: LexOrdering =
422            LexOrdering::new(self.expr[self.partition_prefix_len..].iter().cloned())
423                .expect("PartitionedTopKExec requires at least one order-by expression");
424        let fetch = self.fetch;
425        let fn_kind = self.fn_kind;
426        let batch_size = context.session_config().batch_size();
427        let runtime = Arc::clone(&context.runtime_env());
428        let metrics_set = self.metrics_set.clone();
429
430        let stream = futures::stream::once(async move {
431            do_partitioned_topk(
432                partition,
433                input,
434                schema,
435                partition_exprs,
436                partition_sort_fields,
437                order_expr,
438                fetch,
439                fn_kind,
440                batch_size,
441                runtime,
442                metrics_set,
443            )
444            .await
445        })
446        .try_flatten();
447
448        Ok(Box::pin(RecordBatchStreamAdapter::new(
449            self.input.schema(),
450            stream,
451        )))
452    }
453}
454
455/// Read all input, feed each batch into a per-partition top-K state
456/// (either [`PartitionedTopK`] for `ROW_NUMBER` or
457/// [`PartitionedTopKRank`] for `RANK`), then emit results ordered by
458/// `(partition_keys, order_keys)`.
459///
460/// # Phases
461///
462/// 1. **Accumulation** — forward each input `RecordBatch` to the
463///    per-partition state's `insert_batch`. The `RowConverter` for
464///    ORDER BY columns, the operator's `MemoryReservation`, and the
465///    `TopKMetrics` are shared across all distinct partition keys for
466///    this operator instance.
467///
468/// 2. **Emission** — `emit` drains all per-partition heaps in sorted
469///    partition-key order, returning a coalesced batch stream. For
470///    `RANK`, boundary-tied rows are materialized and emitted after
471///    each partition's heap rows.
472///
473/// # Cost
474///
475/// - Time: O(N log K) where N = total rows, K = fetch
476/// - Memory: O(K × P × row_size) where P = number of distinct partitions
477///   plus, for RANK, the boundary ties' rows
478#[expect(clippy::too_many_arguments)]
479async fn do_partitioned_topk(
480    partition_id: usize,
481    mut input: SendableRecordBatchStream,
482    schema: SchemaRef,
483    partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
484    partition_sort_fields: Vec<SortField>,
485    order_expr: LexOrdering,
486    fetch: usize,
487    fn_kind: WindowFnKind,
488    batch_size: usize,
489    runtime: Arc<RuntimeEnv>,
490    metrics_set: ExecutionPlanMetricsSet,
491) -> Result<SendableRecordBatchStream> {
492    match fn_kind {
493        WindowFnKind::RowNumber => {
494            let mut state = PartitionedTopK::try_new(
495                partition_id,
496                schema,
497                partition_exprs,
498                partition_sort_fields,
499                order_expr,
500                fetch,
501                batch_size,
502                &runtime,
503                &metrics_set,
504            )?;
505            while let Some(batch) = input.next().await {
506                state.insert_batch(&batch?)?;
507            }
508            drop(input);
509            state.emit()
510        }
511        WindowFnKind::Rank => {
512            let mut state = PartitionedTopKRank::try_new(
513                partition_id,
514                schema,
515                partition_exprs,
516                partition_sort_fields,
517                order_expr,
518                fetch,
519                batch_size,
520                &runtime,
521                &metrics_set,
522            )?;
523            while let Some(batch) = input.next().await {
524                state.insert_batch(&batch?)?;
525            }
526            drop(input);
527            state.emit()
528        }
529    }
530}