Skip to content

Commit 78b828c

Browse files
authored
bench: add standard window function cases to bounded_window (#24151)
## Which issue does this PR close? - Related to #23982 ## Rationale for this change The `bounded_window` benchmark only covered aggregate window expressions (`count` and `sum`). Window functions such as `row_number`, `rank`, `lag`, `lead`, and `nth_value` go through a different code path, for which we had no benchmark coverage. Add four new benchmark cases: - `linear dense row_number 10000 partitions`: per-visit fixed costs on partitions that receive rows in every batch. - `linear sparse row_number 32768 partitions`: per-batch work on quiet partitions dominates. - `linear sparse lead 32768 partitions`: a non-causal function, whose result for the last buffered row of a partition stays pending until that partition receives another row. - `linear dense rank 10000 partitions`: an evaluator that compares ORDER BY values row by row. Also rename the existing case names to be consistent. ## What changes are included in this PR? * Add 4 new benchmark cases covering standard (non-agg) window functions * Refactor window benchmark code to enable this * Rename benchmark case names to be mutually consistent ## Are these changes tested? Yes. ## Are there any user-facing changes? No.
1 parent c81e707 commit 78b828c

1 file changed

Lines changed: 142 additions & 29 deletions

File tree

datafusion/physical-plan/benches/bounded_window.rs

Lines changed: 142 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,36 @@
2121
//! `PartitionKey` (`Vec<ScalarValue>`) and, in `Linear` mode (input sorted
2222
//! by the ORDER BY column but not by the partition columns), visits every
2323
//! live partition on every batch while never retiring partitions until the
24-
//! input is exhausted. The cases here stress that path in different ways:
24+
//! input is exhausted. The cases here stress that path in different ways.
2525
//!
26-
//! - `linear N partitions`: dense round-robin keys -- every partition
27-
//! receives rows in every batch, so per-visit fixed costs dominate.
28-
//! - `linear sparse N partitions`: keys are clustered in time, so each
29-
//! batch touches only a small, fresh subset of keys while the set of live
30-
//! partitions keeps growing -- per-batch work on quiet partitions
26+
//! Case names spell out the input order mode (`linear` / `sorted`), the key
27+
//! layout (`dense` / `sparse`), the window functions, an optional frame
28+
//! variant, and the partition count:
29+
//!
30+
//! - `linear dense count N partitions`: dense round-robin keys -- every
31+
//! partition receives rows in every batch, so per-visit fixed costs
32+
//! dominate.
33+
//! - `linear sparse count N partitions`: keys are clustered in time, so
34+
//! each batch touches only a small, fresh subset of keys while the set of
35+
//! live partitions keeps growing -- per-batch work on quiet partitions
3136
//! dominates.
32-
//! - `linear rows N partitions`: the dense layout with a ROWS frame, whose
33-
//! results can only be finalized as more rows of the same partition
34-
//! arrive.
35-
//! - `linear multi N partitions`: two window expressions over the dense
36-
//! layout, doubling the per-partition evaluation sweeps.
37-
//! - `sorted N partitions`: control; input sorted by partition key, so
38-
//! finished partitions are pruned eagerly and the state maps stay small.
37+
//! - `linear dense count rows-frame N partitions`: the dense layout with a
38+
//! ROWS frame, whose results can only be finalized as more rows of the
39+
//! same partition arrive.
40+
//! - `linear dense count+sum N partitions`: two window expressions over the
41+
//! dense layout, doubling the per-partition evaluation sweeps.
42+
//! - `linear dense row_number N partitions` / `linear sparse row_number N
43+
//! partitions`: the dense / sparse layouts evaluated through
44+
//! `StandardWindowExpr` and a `PartitionEvaluator` rather than an
45+
//! aggregate accumulator.
46+
//! - `linear sparse lead N partitions`: the sparse layout with a non-causal
47+
//! function, whose result for the last buffered row of a partition stays
48+
//! pending until that partition receives another row.
49+
//! - `linear dense rank N partitions`: the dense layout with an evaluator
50+
//! that compares ORDER BY values row by row.
51+
//! - `sorted count N partitions`: control; input sorted by partition key,
52+
//! as `Sorted` mode requires, so finished partitions are pruned eagerly
53+
//! and the state maps stay small.
3954
4055
use std::sync::Arc;
4156

@@ -50,8 +65,11 @@ use datafusion_expr::{
5065
};
5166
use datafusion_functions_aggregate::count::count_udaf;
5267
use datafusion_functions_aggregate::sum::sum_udaf;
68+
use datafusion_functions_window::lead_lag::lead_udwf;
69+
use datafusion_functions_window::rank::rank_udwf;
70+
use datafusion_functions_window::row_number::row_number_udwf;
5371
use datafusion_physical_expr::expressions::col;
54-
use datafusion_physical_expr::{LexOrdering, PhysicalSortExpr};
72+
use datafusion_physical_expr::{LexOrdering, PhysicalExpr, PhysicalSortExpr};
5573
use datafusion_physical_plan::test::TestMemoryExec;
5674
use datafusion_physical_plan::windows::{BoundedWindowAggExec, create_window_expr};
5775
use datafusion_physical_plan::{ExecutionPlan, InputOrderMode, collect};
@@ -135,34 +153,46 @@ fn rows_frame() -> WindowFrame {
135153
)
136154
}
137155

138-
/// `<agg>(ts) OVER (PARTITION BY pk ORDER BY ts <window_frame>)` for each
139-
/// aggregate in `aggregates`.
156+
/// `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`, the default frame
157+
/// of a window that has an ORDER BY clause.
158+
fn default_frame() -> WindowFrame {
159+
WindowFrame::new(Some(false))
160+
}
161+
162+
/// A window function to benchmark: definition, display name, and arguments.
163+
type BenchWindowFn = (
164+
WindowFunctionDefinition,
165+
&'static str,
166+
Vec<Arc<dyn PhysicalExpr>>,
167+
);
168+
169+
/// `<fn>(<args>) OVER (PARTITION BY pk ORDER BY ts <window_frame>)` for each
170+
/// window function in `functions`.
140171
fn window_exec(
141172
batches: Vec<RecordBatch>,
142173
mode: InputOrderMode,
143174
input_ordering: Vec<PhysicalSortExpr>,
144175
window_frame: &WindowFrame,
145-
aggregates: &[(WindowFunctionDefinition, &str)],
176+
functions: &[BenchWindowFn],
146177
) -> Arc<dyn ExecutionPlan> {
147178
let schema = schema();
148179
let source = TestMemoryExec::try_new(&[batches], Arc::clone(&schema), None)
149180
.expect("memory exec")
150181
.try_with_sort_information(LexOrdering::new(input_ordering).into_iter().collect())
151182
.expect("sort information");
152183
let input = Arc::new(TestMemoryExec::update_cache(&Arc::new(source)));
153-
let args = vec![col("ts", &schema).unwrap()];
154184
let partitionby_exprs = vec![col("pk", &schema).unwrap()];
155185
let orderby_exprs = vec![PhysicalSortExpr {
156186
expr: col("ts", &schema).unwrap(),
157187
options: Default::default(),
158188
}];
159-
let window_expr = aggregates
189+
let window_expr = functions
160190
.iter()
161-
.map(|(fun, name)| {
191+
.map(|(fun, name, args)| {
162192
create_window_expr(
163193
fun,
164194
name.to_string(),
165-
&args,
195+
args,
166196
&partitionby_exprs,
167197
&orderby_exprs,
168198
Arc::new(window_frame.clone()),
@@ -180,15 +210,48 @@ fn window_exec(
180210
)
181211
}
182212

183-
fn count() -> (WindowFunctionDefinition, &'static str) {
213+
fn ts_arg() -> Vec<Arc<dyn PhysicalExpr>> {
214+
vec![col("ts", &schema()).unwrap()]
215+
}
216+
217+
fn count() -> BenchWindowFn {
184218
(
185219
WindowFunctionDefinition::AggregateUDF(count_udaf()),
186220
"count",
221+
ts_arg(),
187222
)
188223
}
189224

190-
fn sum() -> (WindowFunctionDefinition, &'static str) {
191-
(WindowFunctionDefinition::AggregateUDF(sum_udaf()), "sum")
225+
fn sum() -> BenchWindowFn {
226+
(
227+
WindowFunctionDefinition::AggregateUDF(sum_udaf()),
228+
"sum",
229+
ts_arg(),
230+
)
231+
}
232+
233+
fn row_number() -> BenchWindowFn {
234+
(
235+
WindowFunctionDefinition::WindowUDF(row_number_udwf()),
236+
"row_number",
237+
vec![],
238+
)
239+
}
240+
241+
fn lead() -> BenchWindowFn {
242+
(
243+
WindowFunctionDefinition::WindowUDF(lead_udwf()),
244+
"lead",
245+
ts_arg(),
246+
)
247+
}
248+
249+
fn rank() -> BenchWindowFn {
250+
(
251+
WindowFunctionDefinition::WindowUDF(rank_udwf()),
252+
"rank",
253+
vec![],
254+
)
192255
}
193256

194257
fn bounded_window_benchmark(c: &mut Criterion) {
@@ -213,7 +276,7 @@ fn bounded_window_benchmark(c: &mut Criterion) {
213276

214277
for n_partitions in [100, 10_000] {
215278
run_case(
216-
format!("linear {n_partitions} partitions"),
279+
format!("linear dense count {n_partitions} partitions"),
217280
window_exec(
218281
dense_batches(n_partitions),
219282
InputOrderMode::Linear,
@@ -226,7 +289,7 @@ fn bounded_window_benchmark(c: &mut Criterion) {
226289

227290
run_case(
228291
format!(
229-
"linear sparse {} partitions",
292+
"linear sparse count {} partitions",
230293
N_BATCHES * SPARSE_KEYS_PER_BATCH
231294
),
232295
window_exec(
@@ -239,7 +302,7 @@ fn bounded_window_benchmark(c: &mut Criterion) {
239302
);
240303

241304
run_case(
242-
"linear rows 10000 partitions".to_string(),
305+
"linear dense count rows-frame 10000 partitions".to_string(),
243306
window_exec(
244307
dense_batches(10_000),
245308
InputOrderMode::Linear,
@@ -250,7 +313,7 @@ fn bounded_window_benchmark(c: &mut Criterion) {
250313
);
251314

252315
run_case(
253-
"linear multi 10000 partitions".to_string(),
316+
"linear dense count+sum 10000 partitions".to_string(),
254317
window_exec(
255318
dense_batches(10_000),
256319
InputOrderMode::Linear,
@@ -260,10 +323,60 @@ fn bounded_window_benchmark(c: &mut Criterion) {
260323
),
261324
);
262325

326+
run_case(
327+
"linear dense row_number 10000 partitions".to_string(),
328+
window_exec(
329+
dense_batches(10_000),
330+
InputOrderMode::Linear,
331+
vec![sort_expr("ts")],
332+
&default_frame(),
333+
&[row_number()],
334+
),
335+
);
336+
337+
run_case(
338+
format!(
339+
"linear sparse row_number {} partitions",
340+
N_BATCHES * SPARSE_KEYS_PER_BATCH
341+
),
342+
window_exec(
343+
sparse_batches(),
344+
InputOrderMode::Linear,
345+
vec![sort_expr("ts")],
346+
&default_frame(),
347+
&[row_number()],
348+
),
349+
);
350+
351+
run_case(
352+
format!(
353+
"linear sparse lead {} partitions",
354+
N_BATCHES * SPARSE_KEYS_PER_BATCH
355+
),
356+
window_exec(
357+
sparse_batches(),
358+
InputOrderMode::Linear,
359+
vec![sort_expr("ts")],
360+
&default_frame(),
361+
&[lead()],
362+
),
363+
);
364+
365+
run_case(
366+
"linear dense rank 10000 partitions".to_string(),
367+
window_exec(
368+
dense_batches(10_000),
369+
InputOrderMode::Linear,
370+
vec![sort_expr("ts")],
371+
&default_frame(),
372+
&[rank()],
373+
),
374+
);
375+
263376
// Control: the same query over partition-sorted input, where finished
264377
// partitions are pruned eagerly and the state maps stay small.
265378
run_case(
266-
"sorted 10000 partitions".to_string(),
379+
"sorted count 10000 partitions".to_string(),
267380
window_exec(
268381
sorted_batches(10_000),
269382
InputOrderMode::Sorted,

0 commit comments

Comments
 (0)