Skip to content

Commit d0c0314

Browse files
committed
perf: Extend WindowTopN to dense rank
1 parent 95de385 commit d0c0314

6 files changed

Lines changed: 1387 additions & 47 deletions

File tree

benchmarks/queries/h2o/window.sql

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,53 @@ SELECT pk, largest_v2 FROM (
196196
RANK() OVER (PARTITION BY (id3 % 100000) ORDER BY v2 DESC) AS rk_v2
197197
FROM large WHERE v2 IS NOT NULL
198198
) sub_query WHERE rk_v2 <= 2;
199+
200+
-- Window Top-N (DENSE_RANK top-2 per partition, ~100 partitions)
201+
-- The DENSE_RANK queries below mirror the RANK cardinality sweep above.
202+
-- DENSE_RANK semantics keep every row whose ORDER BY value is among the
203+
-- K distinct-smallest values in the partition, so total kept per partition
204+
-- is unbounded in rows-per-distinct-value — exercises PartitionedTopKDenseRank's
205+
-- HashMap-of-groups path.
206+
SELECT pk, largest_v2 FROM (
207+
SELECT (id3 % 100) AS pk, v2 AS largest_v2,
208+
DENSE_RANK() OVER (PARTITION BY (id3 % 100) ORDER BY v2 DESC) AS dr_v2
209+
FROM large WHERE v2 IS NOT NULL
210+
) sub_query WHERE dr_v2 <= 2;
211+
212+
-- Window Top-N (DENSE_RANK top-2 per partition, ~1K partitions)
213+
SELECT pkey, largest_v2 FROM (
214+
SELECT (id3 % 1000) AS pkey, v2 AS largest_v2,
215+
DENSE_RANK() OVER (PARTITION BY (id3 % 1000) ORDER BY v2 DESC) AS dr_v2
216+
FROM large WHERE v2 IS NOT NULL
217+
) sub_query WHERE dr_v2 <= 2;
218+
219+
-- Window Top-N (DENSE_RANK top-2 per partition, ~1K partitions, heavy ties)
220+
-- v2 % 10 forces 10 distinct OBY values; most rows share the top-2 distinct
221+
-- values so appends dominate — exercises the "Case A" append-to-existing-Vec
222+
-- fast path in PartitionedTopKDenseRank.
223+
SELECT pkey, largest_v2 FROM (
224+
SELECT (id3 % 1000) AS pkey, v2 AS largest_v2,
225+
DENSE_RANK() OVER (PARTITION BY (id3 % 1000) ORDER BY (v2 % 10) DESC) AS dr_v2
226+
FROM large WHERE v2 IS NOT NULL
227+
) sub_query WHERE dr_v2 <= 2;
228+
229+
-- Window Top-N (DENSE_RANK top-2 per partition, ~10K partitions, low ties)
230+
SELECT id2, largest_v2 FROM (
231+
SELECT id2, v2 AS largest_v2,
232+
DENSE_RANK() OVER (PARTITION BY id2 ORDER BY v2 DESC) AS dr_v2
233+
FROM large WHERE v2 IS NOT NULL
234+
) sub_query WHERE dr_v2 <= 2;
235+
236+
-- Window Top-N (DENSE_RANK top-2 per partition, ~10K partitions, heavy ties)
237+
SELECT id2, largest_v2 FROM (
238+
SELECT id2, v2 AS largest_v2,
239+
DENSE_RANK() OVER (PARTITION BY id2 ORDER BY (v2 % 10) DESC) AS dr_v2
240+
FROM large WHERE v2 IS NOT NULL
241+
) sub_query WHERE dr_v2 <= 2;
242+
243+
-- Window Top-N (DENSE_RANK top-2 per partition, ~100K partitions)
244+
SELECT pk, largest_v2 FROM (
245+
SELECT (id3 % 100000) AS pk, v2 AS largest_v2,
246+
DENSE_RANK() OVER (PARTITION BY (id3 % 100000) ORDER BY v2 DESC) AS dr_v2
247+
FROM large WHERE v2 IS NOT NULL
248+
) sub_query WHERE dr_v2 <= 2;

datafusion/core/tests/physical_optimizer/window_topn.rs

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -488,8 +488,12 @@ fn build_ranking_topn_plan(
488488
Ok(filter)
489489
}
490490

491-
/// Build a RANK plan with NO ORDER BY: every row ties at rank 1 — degenerate.
492-
fn build_rank_no_order_by_plan(limit_value: i64) -> Result<Arc<dyn ExecutionPlan>> {
491+
/// Build a RANK / DENSE_RANK plan with NO ORDER BY: every row ties at rank 1 — degenerate.
492+
fn build_no_order_by_plan(
493+
udwf_factory: fn() -> Arc<datafusion_expr::WindowUDF>,
494+
udwf_name: &str,
495+
limit_value: i64,
496+
) -> Result<Arc<dyn ExecutionPlan>> {
493497
let s = schema();
494498
let input: Arc<dyn ExecutionPlan> = Arc::new(PlaceholderRowExec::new(Arc::clone(&s)));
495499

@@ -503,7 +507,7 @@ fn build_rank_no_order_by_plan(limit_value: i64) -> Result<Arc<dyn ExecutionPlan
503507
let partition_by = vec![col("pk", &s)?];
504508

505509
let window_expr = Arc::new(StandardWindowExpr::new(
506-
create_udwf_window_expr(&rank_udwf(), &[], &s, "rank".to_string(), false)?,
510+
create_udwf_window_expr(&udwf_factory(), &[], &s, udwf_name.to_string(), false)?,
507511
&partition_by,
508512
&[], // empty ORDER BY
509513
Arc::new(WindowFrame::new_bounds(
@@ -520,7 +524,7 @@ fn build_rank_no_order_by_plan(limit_value: i64) -> Result<Arc<dyn ExecutionPlan
520524
true,
521525
)?);
522526

523-
let rk_col = Arc::new(Column::new("rank", 2));
527+
let rk_col = Arc::new(Column::new(udwf_name, 2));
524528
let limit_lit = lit(ScalarValue::UInt64(Some(limit_value as u64)));
525529
let predicate = Arc::new(BinaryExpr::new(rk_col, Operator::LtEq, limit_lit));
526530
let filter: Arc<dyn ExecutionPlan> =
@@ -582,7 +586,7 @@ fn rank_no_order_by_no_change() -> Result<()> {
582586
// Without ORDER BY, every row ties at rank 1 — the optimization is
583587
// degenerate (entire input would be retained, ties storage unbounded).
584588
// The rule must skip.
585-
let plan = build_rank_no_order_by_plan(3)?;
589+
let plan = build_no_order_by_plan(rank_udwf, "rank", 3)?;
586590
let before = plan_str(plan.as_ref());
587591
let optimized = optimize(plan)?;
588592
let after = plan_str(optimized.as_ref());
@@ -593,17 +597,98 @@ fn rank_no_order_by_no_change() -> Result<()> {
593597
Ok(())
594598
}
595599

600+
// ----------------------------------------------------------------------
601+
// DENSE_RANK rule tests
602+
// ----------------------------------------------------------------------
603+
596604
#[test]
597-
fn dense_rank_no_change() -> Result<()> {
598-
// DENSE_RANK is not yet supported by the rule. The plan must pass
599-
// through unchanged.
605+
fn basic_dense_rank_dr_lteq_3() -> Result<()> {
600606
let plan = build_ranking_topn_plan(dense_rank_udwf, "dense_rank", 3, Operator::LtEq)?;
607+
let optimized = optimize(plan)?;
608+
assert_snapshot!(plan_str(optimized.as_ref()), @r#"
609+
BoundedWindowAggExec: wdw=[dense_rank: Field { "dense_rank": UInt64 }, frame: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW], mode=[Sorted]
610+
PartitionedTopKExec: fn=dense_rank, fetch=3, partition=[pk@0], order=[val@1 ASC]
611+
PlaceholderRowExec
612+
"#);
613+
Ok(())
614+
}
615+
616+
#[test]
617+
fn dense_rank_dr_lt_4_becomes_fetch_3() -> Result<()> {
618+
let plan = build_ranking_topn_plan(dense_rank_udwf, "dense_rank", 4, Operator::Lt)?;
619+
let optimized = optimize(plan)?;
620+
assert_snapshot!(plan_str(optimized.as_ref()), @r#"
621+
BoundedWindowAggExec: wdw=[dense_rank: Field { "dense_rank": UInt64 }, frame: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW], mode=[Sorted]
622+
PartitionedTopKExec: fn=dense_rank, fetch=3, partition=[pk@0], order=[val@1 ASC]
623+
PlaceholderRowExec
624+
"#);
625+
Ok(())
626+
}
627+
628+
#[test]
629+
fn dense_rank_flipped_3_gteq_dr() -> Result<()> {
630+
let plan = build_ranking_topn_plan(dense_rank_udwf, "dense_rank", 3, Operator::GtEq)?;
631+
let optimized = optimize(plan)?;
632+
assert_snapshot!(plan_str(optimized.as_ref()), @r#"
633+
BoundedWindowAggExec: wdw=[dense_rank: Field { "dense_rank": UInt64 }, frame: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW], mode=[Sorted]
634+
PartitionedTopKExec: fn=dense_rank, fetch=3, partition=[pk@0], order=[val@1 ASC]
635+
PlaceholderRowExec
636+
"#);
637+
Ok(())
638+
}
639+
640+
#[test]
641+
fn dense_rank_flipped_4_gt_dr_becomes_fetch_3() -> Result<()> {
642+
let plan = build_ranking_topn_plan(dense_rank_udwf, "dense_rank", 4, Operator::Gt)?;
643+
let optimized = optimize(plan)?;
644+
assert_snapshot!(plan_str(optimized.as_ref()), @r#"
645+
BoundedWindowAggExec: wdw=[dense_rank: Field { "dense_rank": UInt64 }, frame: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW], mode=[Sorted]
646+
PartitionedTopKExec: fn=dense_rank, fetch=3, partition=[pk@0], order=[val@1 ASC]
647+
PlaceholderRowExec
648+
"#);
649+
Ok(())
650+
}
651+
652+
#[test]
653+
fn dense_rank_no_order_by_no_change() -> Result<()> {
654+
// Without ORDER BY, every row ties at dense_rank 1 — the optimization
655+
// is degenerate (entire input would be retained). The rule must skip.
656+
let plan = build_no_order_by_plan(dense_rank_udwf, "dense_rank", 3)?;
601657
let before = plan_str(plan.as_ref());
602658
let optimized = optimize(plan)?;
603659
let after = plan_str(optimized.as_ref());
604660
assert_eq!(
605661
before, after,
606-
"DENSE_RANK is unsupported and must not be rewritten"
662+
"DENSE_RANK with empty ORDER BY must not be rewritten"
607663
);
608664
Ok(())
609665
}
666+
667+
// ----------------------------------------------------------------------
668+
// Shared guard: `fn < 1` keeps nothing
669+
// ----------------------------------------------------------------------
670+
671+
#[test]
672+
fn predicate_lt_1_no_change() -> Result<()> {
673+
// `fn < 1` (and the flipped `1 > fn`) yields limit_n = 0. Since
674+
// ROW_NUMBER / RANK / DENSE_RANK are always >= 1, the predicate keeps
675+
// nothing and the rule must skip — a fetch=0 PartitionedTopK* would
676+
// otherwise panic on its `k > 0` assertion at execution time.
677+
type UdwfFactory = fn() -> Arc<datafusion_expr::WindowUDF>;
678+
let cases: [(UdwfFactory, &str); 3] = [
679+
(row_number_udwf, "row_number"),
680+
(rank_udwf, "rank"),
681+
(dense_rank_udwf, "dense_rank"),
682+
];
683+
for (factory, name) in cases {
684+
let plan = build_ranking_topn_plan(factory, name, 1, Operator::Lt)?;
685+
let before = plan_str(plan.as_ref());
686+
let optimized = optimize(plan)?;
687+
let after = plan_str(optimized.as_ref());
688+
assert_eq!(
689+
before, after,
690+
"`{name} < 1` (limit 0) must not be rewritten"
691+
);
692+
}
693+
Ok(())
694+
}

datafusion/physical-optimizer/src/window_topn.rs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
//! ) WHERE rn <= K;
2727
//! ```
2828
//!
29-
//! or with `RANK()` in place of `ROW_NUMBER()`:
29+
//! or with `RANK()` / `DENSE_RANK()` in place of `ROW_NUMBER()`:
3030
//!
3131
//! ```sql
3232
//! SELECT * FROM (
@@ -40,8 +40,8 @@
4040
//! the `FilterExec` and `SortExec`.
4141
//!
4242
//! The appropriate [`WindowFnKind`] is forwarded to `PartitionedTopKExec`.
43-
//! RANK requires a non-empty `ORDER BY` clause (otherwise all rows tie at
44-
//! rank 1 and the optimization is degenerate).
43+
//! `RANK` and `DENSE_RANK` require a non-empty `ORDER BY` clause (otherwise
44+
//! all rows tie at rank 1 and the optimization is degenerate).
4545
//!
4646
//! See [`PartitionedTopKExec`] for details on the replacement operator.
4747
//!
@@ -68,9 +68,9 @@ use datafusion_physical_plan::sorts::partitioned_topk::{
6868
use datafusion_physical_plan::sorts::sort::SortExec;
6969
use datafusion_physical_plan::windows::{BoundedWindowAggExec, WindowUDFExpr};
7070

71-
/// Physical optimizer rule that converts per-partition `ROW_NUMBER` and
72-
/// `RANK` top-K queries into a more efficient plan using
73-
/// [`PartitionedTopKExec`].
71+
/// Physical optimizer rule that converts per-partition `ROW_NUMBER`,
72+
/// `RANK`, and `DENSE_RANK` top-K queries into a more efficient plan
73+
/// using [`PartitionedTopKExec`].
7474
///
7575
/// # Pattern Detected
7676
///
@@ -86,12 +86,14 @@ use datafusion_physical_plan::windows::{BoundedWindowAggExec, WindowUDFExpr};
8686
/// ```text
8787
/// [optional ProjectionExec]
8888
/// BoundedWindowAggExec(<ranking fn> PARTITION BY ... ORDER BY ...)
89-
/// PartitionedTopKExec(fn=<row_number|rank>, partition_keys, order_keys, fetch=K)
89+
/// PartitionedTopKExec(fn=<row_number|rank|dense_rank>, partition_keys, order_keys, fetch=K)
9090
/// ```
9191
///
9292
/// The `FilterExec` is removed entirely. The `SortExec` is replaced by
93-
/// `PartitionedTopKExec`, which maintains a per-partition top-K heap (and,
94-
/// for `RANK`, a sibling ties `Vec`) instead of sorting the whole dataset.
93+
/// `PartitionedTopKExec`, which maintains per-partition top-K state (a
94+
/// heap for `ROW_NUMBER`, a heap plus boundary ties for `RANK`, a
95+
/// K-bounded distinct-ob map for `DENSE_RANK`) instead of sorting the
96+
/// whole dataset.
9597
///
9698
/// # Supported Predicates
9799
///
@@ -105,12 +107,12 @@ use datafusion_physical_plan::windows::{BoundedWindowAggExec, WindowUDFExpr};
105107
/// All of the following must be true:
106108
/// - Config flag `enable_window_topn` is `true`
107109
/// - The plan matches `FilterExec → [ProjectionExec] → BoundedWindowAggExec → SortExec`
108-
/// - The window function is `ROW_NUMBER` or `RANK` (not `DENSE_RANK`)
110+
/// - The window function is `ROW_NUMBER`, `RANK`, or `DENSE_RANK`
109111
/// - The window function has a `PARTITION BY` clause (global top-K is
110112
/// already handled by `SortExec` with `fetch`)
111-
/// - For `RANK`: a non-empty `ORDER BY` clause (otherwise all rows tie
112-
/// at rank 1 — the optimization is useless and the boundary-tie storage
113-
/// would be unbounded)
113+
/// - For `RANK` / `DENSE_RANK`: a non-empty `ORDER BY` clause (otherwise
114+
/// all rows tie at rank 1 — the optimization is useless and the
115+
/// retained set would be unbounded)
114116
/// - The filter predicate compares the window output column to an integer
115117
/// literal using `<=`, `<`, `>=`, or `>`
116118
///
@@ -141,6 +143,14 @@ impl WindowTopN {
141143
// Step 2: Extract limit from predicate (rn <= K, rn < K, etc.)
142144
let (col_idx, limit_n) = extract_window_limit(filter.predicate())?;
143145

146+
// `fn < 1` (or the flipped `1 > fn`) yields limit_n = 0. Since
147+
// ROW_NUMBER / RANK / DENSE_RANK are always >= 1, such a predicate
148+
// keeps nothing — bail out and let the ordinary FilterExec return
149+
// the empty result. (The PartitionedTopK* operators require k > 0.)
150+
if limit_n == 0 {
151+
return None;
152+
}
153+
144154
// Step 3: Walk through optional ProjectionExec and RepartitionExec to find BoundedWindowAggExec
145155
let child = filter.input();
146156
let (window_exec, intermediates) = find_window_below(child)?;
@@ -172,10 +182,10 @@ impl WindowTopN {
172182
return None;
173183
}
174184

175-
// For RANK: an empty ORDER BY makes every row tie at rank 1 —
176-
// the optimization is degenerate (we'd retain the entire input)
177-
// and tie storage would be unbounded.
178-
if matches!(fn_kind, WindowFnKind::Rank)
185+
// For RANK / DENSE_RANK: an empty ORDER BY makes every row tie
186+
// at rank 1 — the optimization is degenerate (we'd retain the
187+
// entire input) and tie storage would be unbounded.
188+
if matches!(fn_kind, WindowFnKind::Rank | WindowFnKind::DenseRank)
179189
&& window_exprs[window_expr_idx].order_by().is_empty()
180190
{
181191
return None;
@@ -315,7 +325,8 @@ fn scalar_to_usize(value: &ScalarValue) -> Option<usize> {
315325
/// the UDF name. Returns:
316326
/// - `Some(WindowFnKind::RowNumber)` for `"row_number"`
317327
/// - `Some(WindowFnKind::Rank)` for `"rank"`
318-
/// - `None` for everything else (e.g. `dense_rank`)
328+
/// - `Some(WindowFnKind::DenseRank)` for `"dense_rank"`
329+
/// - `None` for everything else
319330
fn supported_window_fn(
320331
expr: &Arc<dyn datafusion_physical_expr::window::WindowExpr>,
321332
) -> Option<WindowFnKind> {
@@ -325,6 +336,7 @@ fn supported_window_fn(
325336
match udf.fun().name() {
326337
"row_number" => Some(WindowFnKind::RowNumber),
327338
"rank" => Some(WindowFnKind::Rank),
339+
"dense_rank" => Some(WindowFnKind::DenseRank),
328340
_ => None,
329341
}
330342
}

0 commit comments

Comments
 (0)