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}