Skip to content

Commit 43cc870

Browse files
msirekMark Sirekalamb
authored
Push limit into aggregation for DISTINCT ... LIMIT queries (#8038)
* Push limit into AggregateExec for DISTINCT with GROUP BY * Soft limit for GroupedHashAggregateStream with no aggregate expressions * Add datafusion.optimizer.enable_distinct_aggregation_soft_limit setting * Fix result checking in topk_aggregate benchmark * Make the topk_aggregate benchmark's make_data function public * Add benchmark for DISTINCT queries * Fix doc formatting with prettier * Minor: Simply early emit logic in GroupByHash * remove level of indentation * Use '///' for function comments * Address review comments * rename transform_local_limit to transform_limit * Resolve conflicts * Update test after merge with main --------- Co-authored-by: Mark Sirek <sirek@cockroachlabs.com> Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
1 parent 1c17c47 commit 43cc870

19 files changed

Lines changed: 1299 additions & 124 deletions

File tree

datafusion/common/src/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,11 @@ config_namespace! {
427427
config_namespace! {
428428
/// Options related to query optimization
429429
pub struct OptimizerOptions {
430+
/// When set to true, the optimizer will push a limit operation into
431+
/// grouped aggregations which have no aggregate expressions, as a soft limit,
432+
/// emitting groups once the limit is reached, before all rows in the group are read.
433+
pub enable_distinct_aggregation_soft_limit: bool, default = true
434+
430435
/// When set to true, the physical plan optimizer will try to add round robin
431436
/// repartitioning to increase parallelism to leverage more CPU cores
432437
pub enable_round_robin_repartition: bool, default = true

datafusion/common/src/tree_node.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,17 @@ pub trait TreeNode: Sized {
125125
after_op.map_children(|node| node.transform_down(op))
126126
}
127127

128+
/// Convenience utils for writing optimizers rule: recursively apply the given 'op' to the node and all of its
129+
/// children(Preorder Traversal) using a mutable function, `F`.
130+
/// When the `op` does not apply to a given node, it is left unchanged.
131+
fn transform_down_mut<F>(self, op: &mut F) -> Result<Self>
132+
where
133+
F: FnMut(Self) -> Result<Transformed<Self>>,
134+
{
135+
let after_op = op(self)?.into();
136+
after_op.map_children(|node| node.transform_down_mut(op))
137+
}
138+
128139
/// Convenience utils for writing optimizers rule: recursively apply the given 'op' first to all of its
129140
/// children and then itself(Postorder Traversal).
130141
/// When the `op` does not apply to a given node, it is left unchanged.

datafusion/core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ nix = { version = "0.27.1", features = ["fs"] }
120120
harness = false
121121
name = "aggregate_query_sql"
122122

123+
[[bench]]
124+
harness = false
125+
name = "distinct_query_sql"
126+
123127
[[bench]]
124128
harness = false
125129
name = "sort_limit_query_sql"

datafusion/core/benches/data_utils/mod.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,16 @@ use arrow::{
2525
datatypes::{DataType, Field, Schema, SchemaRef},
2626
record_batch::RecordBatch,
2727
};
28+
use arrow_array::builder::{Int64Builder, StringBuilder};
2829
use datafusion::datasource::MemTable;
2930
use datafusion::error::Result;
31+
use datafusion_common::DataFusionError;
3032
use rand::rngs::StdRng;
3133
use rand::seq::SliceRandom;
3234
use rand::{Rng, SeedableRng};
35+
use rand_distr::Distribution;
36+
use rand_distr::{Normal, Pareto};
37+
use std::fmt::Write;
3338
use std::sync::Arc;
3439

3540
/// create an in-memory table given the partition len, array len, and batch size,
@@ -156,3 +161,83 @@ pub fn create_record_batches(
156161
})
157162
.collect::<Vec<_>>()
158163
}
164+
165+
/// Create time series data with `partition_cnt` partitions and `sample_cnt` rows per partition
166+
/// in ascending order, if `asc` is true, otherwise randomly sampled using a Pareto distribution
167+
#[allow(dead_code)]
168+
pub(crate) fn make_data(
169+
partition_cnt: i32,
170+
sample_cnt: i32,
171+
asc: bool,
172+
) -> Result<(Arc<Schema>, Vec<Vec<RecordBatch>>), DataFusionError> {
173+
// constants observed from trace data
174+
let simultaneous_group_cnt = 2000;
175+
let fitted_shape = 12f64;
176+
let fitted_scale = 5f64;
177+
let mean = 0.1;
178+
let stddev = 1.1;
179+
let pareto = Pareto::new(fitted_scale, fitted_shape).unwrap();
180+
let normal = Normal::new(mean, stddev).unwrap();
181+
let mut rng = rand::rngs::SmallRng::from_seed([0; 32]);
182+
183+
// populate data
184+
let schema = test_schema();
185+
let mut partitions = vec![];
186+
let mut cur_time = 16909000000000i64;
187+
for _ in 0..partition_cnt {
188+
let mut id_builder = StringBuilder::new();
189+
let mut ts_builder = Int64Builder::new();
190+
let gen_id = |rng: &mut rand::rngs::SmallRng| {
191+
rng.gen::<[u8; 16]>()
192+
.iter()
193+
.fold(String::new(), |mut output, b| {
194+
let _ = write!(output, "{b:02X}");
195+
output
196+
})
197+
};
198+
let gen_sample_cnt =
199+
|mut rng: &mut rand::rngs::SmallRng| pareto.sample(&mut rng).ceil() as u32;
200+
let mut group_ids = (0..simultaneous_group_cnt)
201+
.map(|_| gen_id(&mut rng))
202+
.collect::<Vec<_>>();
203+
let mut group_sample_cnts = (0..simultaneous_group_cnt)
204+
.map(|_| gen_sample_cnt(&mut rng))
205+
.collect::<Vec<_>>();
206+
for _ in 0..sample_cnt {
207+
let random_index = rng.gen_range(0..simultaneous_group_cnt);
208+
let trace_id = &mut group_ids[random_index];
209+
let sample_cnt = &mut group_sample_cnts[random_index];
210+
*sample_cnt -= 1;
211+
if *sample_cnt == 0 {
212+
*trace_id = gen_id(&mut rng);
213+
*sample_cnt = gen_sample_cnt(&mut rng);
214+
}
215+
216+
id_builder.append_value(trace_id);
217+
ts_builder.append_value(cur_time);
218+
219+
if asc {
220+
cur_time += 1;
221+
} else {
222+
let samp: f64 = normal.sample(&mut rng);
223+
let samp = samp.round();
224+
cur_time += samp as i64;
225+
}
226+
}
227+
228+
// convert to MemTable
229+
let id_col = Arc::new(id_builder.finish());
230+
let ts_col = Arc::new(ts_builder.finish());
231+
let batch = RecordBatch::try_new(schema.clone(), vec![id_col, ts_col])?;
232+
partitions.push(vec![batch]);
233+
}
234+
Ok((schema, partitions))
235+
}
236+
237+
/// The Schema used by make_data
238+
fn test_schema() -> SchemaRef {
239+
Arc::new(Schema::new(vec![
240+
Field::new("trace_id", DataType::Utf8, false),
241+
Field::new("timestamp_ms", DataType::Int64, false),
242+
]))
243+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
#[macro_use]
19+
extern crate criterion;
20+
extern crate arrow;
21+
extern crate datafusion;
22+
23+
mod data_utils;
24+
use crate::criterion::Criterion;
25+
use data_utils::{create_table_provider, make_data};
26+
use datafusion::execution::context::SessionContext;
27+
use datafusion::physical_plan::{collect, ExecutionPlan};
28+
use datafusion::{datasource::MemTable, error::Result};
29+
use datafusion_execution::config::SessionConfig;
30+
use datafusion_execution::TaskContext;
31+
32+
use parking_lot::Mutex;
33+
use std::{sync::Arc, time::Duration};
34+
use tokio::runtime::Runtime;
35+
36+
fn query(ctx: Arc<Mutex<SessionContext>>, sql: &str) {
37+
let rt = Runtime::new().unwrap();
38+
let df = rt.block_on(ctx.lock().sql(sql)).unwrap();
39+
criterion::black_box(rt.block_on(df.collect()).unwrap());
40+
}
41+
42+
fn create_context(
43+
partitions_len: usize,
44+
array_len: usize,
45+
batch_size: usize,
46+
) -> Result<Arc<Mutex<SessionContext>>> {
47+
let ctx = SessionContext::new();
48+
let provider = create_table_provider(partitions_len, array_len, batch_size)?;
49+
ctx.register_table("t", provider)?;
50+
Ok(Arc::new(Mutex::new(ctx)))
51+
}
52+
53+
fn criterion_benchmark_limited_distinct(c: &mut Criterion) {
54+
let partitions_len = 10;
55+
let array_len = 1 << 26; // 64 M
56+
let batch_size = 8192;
57+
let ctx = create_context(partitions_len, array_len, batch_size).unwrap();
58+
59+
let mut group = c.benchmark_group("custom-measurement-time");
60+
group.measurement_time(Duration::from_secs(40));
61+
62+
group.bench_function("distinct_group_by_u64_narrow_limit_10", |b| {
63+
b.iter(|| {
64+
query(
65+
ctx.clone(),
66+
"SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 10",
67+
)
68+
})
69+
});
70+
71+
group.bench_function("distinct_group_by_u64_narrow_limit_100", |b| {
72+
b.iter(|| {
73+
query(
74+
ctx.clone(),
75+
"SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 100",
76+
)
77+
})
78+
});
79+
80+
group.bench_function("distinct_group_by_u64_narrow_limit_1000", |b| {
81+
b.iter(|| {
82+
query(
83+
ctx.clone(),
84+
"SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 1000",
85+
)
86+
})
87+
});
88+
89+
group.bench_function("distinct_group_by_u64_narrow_limit_10000", |b| {
90+
b.iter(|| {
91+
query(
92+
ctx.clone(),
93+
"SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 10000",
94+
)
95+
})
96+
});
97+
98+
group.bench_function("group_by_multiple_columns_limit_10", |b| {
99+
b.iter(|| {
100+
query(
101+
ctx.clone(),
102+
"SELECT u64_narrow, u64_wide, utf8, f64 FROM t GROUP BY 1, 2, 3, 4 LIMIT 10",
103+
)
104+
})
105+
});
106+
group.finish();
107+
}
108+
109+
async fn distinct_with_limit(
110+
plan: Arc<dyn ExecutionPlan>,
111+
ctx: Arc<TaskContext>,
112+
) -> Result<()> {
113+
let batches = collect(plan, ctx).await?;
114+
assert_eq!(batches.len(), 1);
115+
let batch = batches.first().unwrap();
116+
assert_eq!(batch.num_rows(), 10);
117+
118+
Ok(())
119+
}
120+
121+
fn run(plan: Arc<dyn ExecutionPlan>, ctx: Arc<TaskContext>) {
122+
let rt = Runtime::new().unwrap();
123+
criterion::black_box(
124+
rt.block_on(async { distinct_with_limit(plan.clone(), ctx.clone()).await }),
125+
)
126+
.unwrap();
127+
}
128+
129+
pub async fn create_context_sampled_data(
130+
sql: &str,
131+
partition_cnt: i32,
132+
sample_cnt: i32,
133+
) -> Result<(Arc<dyn ExecutionPlan>, Arc<TaskContext>)> {
134+
let (schema, parts) = make_data(partition_cnt, sample_cnt, false /* asc */).unwrap();
135+
let mem_table = Arc::new(MemTable::try_new(schema, parts).unwrap());
136+
137+
// Create the DataFrame
138+
let cfg = SessionConfig::new();
139+
let ctx = SessionContext::new_with_config(cfg);
140+
let _ = ctx.register_table("traces", mem_table)?;
141+
let df = ctx.sql(sql).await?;
142+
let physical_plan = df.create_physical_plan().await?;
143+
Ok((physical_plan, ctx.task_ctx()))
144+
}
145+
146+
fn criterion_benchmark_limited_distinct_sampled(c: &mut Criterion) {
147+
let rt = Runtime::new().unwrap();
148+
149+
let limit = 10;
150+
let partitions = 100;
151+
let samples = 100_000;
152+
let sql =
153+
format!("select DISTINCT trace_id from traces group by trace_id limit {limit};");
154+
155+
let distinct_trace_id_100_partitions_100_000_samples_limit_100 = rt.block_on(async {
156+
create_context_sampled_data(sql.as_str(), partitions, samples)
157+
.await
158+
.unwrap()
159+
});
160+
161+
c.bench_function(
162+
format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(),
163+
|b| b.iter(|| run(distinct_trace_id_100_partitions_100_000_samples_limit_100.0.clone(),
164+
distinct_trace_id_100_partitions_100_000_samples_limit_100.1.clone())),
165+
);
166+
167+
let partitions = 10;
168+
let samples = 1_000_000;
169+
let sql =
170+
format!("select DISTINCT trace_id from traces group by trace_id limit {limit};");
171+
172+
let distinct_trace_id_10_partitions_1_000_000_samples_limit_10 = rt.block_on(async {
173+
create_context_sampled_data(sql.as_str(), partitions, samples)
174+
.await
175+
.unwrap()
176+
});
177+
178+
c.bench_function(
179+
format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(),
180+
|b| b.iter(|| run(distinct_trace_id_10_partitions_1_000_000_samples_limit_10.0.clone(),
181+
distinct_trace_id_10_partitions_1_000_000_samples_limit_10.1.clone())),
182+
);
183+
184+
let partitions = 1;
185+
let samples = 10_000_000;
186+
let sql =
187+
format!("select DISTINCT trace_id from traces group by trace_id limit {limit};");
188+
189+
let rt = Runtime::new().unwrap();
190+
let distinct_trace_id_1_partition_10_000_000_samples_limit_10 = rt.block_on(async {
191+
create_context_sampled_data(sql.as_str(), partitions, samples)
192+
.await
193+
.unwrap()
194+
});
195+
196+
c.bench_function(
197+
format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(),
198+
|b| b.iter(|| run(distinct_trace_id_1_partition_10_000_000_samples_limit_10.0.clone(),
199+
distinct_trace_id_1_partition_10_000_000_samples_limit_10.1.clone())),
200+
);
201+
}
202+
203+
criterion_group!(
204+
benches,
205+
criterion_benchmark_limited_distinct,
206+
criterion_benchmark_limited_distinct_sampled
207+
);
208+
criterion_main!(benches);

0 commit comments

Comments
 (0)