Skip to content

Commit 217fa99

Browse files
tustvoldalamb
andauthored
Add parquet SQL benchmarks (#1738)
* Add parquet SQL benchmarks * Restrict benchmark value ranges * Add RAT exclude * appease clippy Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
1 parent 7a2cbd5 commit 217fa99

4 files changed

Lines changed: 274 additions & 0 deletions

File tree

datafusion/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,7 @@ harness = false
116116
[[bench]]
117117
name = "physical_plan"
118118
harness = false
119+
120+
[[bench]]
121+
name = "parquet_query_sql"
122+
harness = false
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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+
//! Benchmarks of SQL queries again parquet data
19+
20+
use arrow::array::{ArrayRef, DictionaryArray, PrimitiveArray, StringArray};
21+
use arrow::datatypes::{
22+
ArrowPrimitiveType, DataType, Field, Float64Type, Int32Type, Int64Type, Schema,
23+
SchemaRef,
24+
};
25+
use arrow::record_batch::RecordBatch;
26+
use criterion::{criterion_group, criterion_main, Criterion};
27+
use datafusion::prelude::ExecutionContext;
28+
use parquet::arrow::ArrowWriter;
29+
use parquet::file::properties::{WriterProperties, WriterVersion};
30+
use rand::distributions::uniform::SampleUniform;
31+
use rand::distributions::Alphanumeric;
32+
use rand::prelude::*;
33+
use std::fs::File;
34+
use std::io::Read;
35+
use std::ops::Range;
36+
use std::path::Path;
37+
use std::sync::Arc;
38+
use std::time::Instant;
39+
use tempfile::NamedTempFile;
40+
use tokio_stream::StreamExt;
41+
42+
/// The number of batches to write
43+
const NUM_BATCHES: usize = 2048;
44+
/// The number of rows in each record batch to write
45+
const WRITE_RECORD_BATCH_SIZE: usize = 1024;
46+
/// The number of rows in a row group
47+
const ROW_GROUP_SIZE: usize = 1024 * 1024;
48+
/// The number of row groups expected
49+
const EXPECTED_ROW_GROUPS: usize = 2;
50+
51+
fn schema() -> SchemaRef {
52+
let string_dictionary_type =
53+
DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8));
54+
55+
Arc::new(Schema::new(vec![
56+
Field::new("dict_10_required", string_dictionary_type.clone(), false),
57+
Field::new("dict_10_optional", string_dictionary_type.clone(), true),
58+
Field::new("dict_100_required", string_dictionary_type.clone(), false),
59+
Field::new("dict_100_optional", string_dictionary_type.clone(), true),
60+
Field::new("dict_1000_required", string_dictionary_type.clone(), false),
61+
Field::new("dict_1000_optional", string_dictionary_type, true),
62+
Field::new("string_required", DataType::Utf8, false),
63+
Field::new("string_optional", DataType::Utf8, true),
64+
Field::new("i64_required", DataType::Int64, false),
65+
Field::new("i64_optional", DataType::Int64, true),
66+
Field::new("f64_required", DataType::Float64, false),
67+
Field::new("f64_optional", DataType::Float64, true),
68+
]))
69+
}
70+
71+
fn generate_batch() -> RecordBatch {
72+
let schema = schema();
73+
let len = WRITE_RECORD_BATCH_SIZE;
74+
RecordBatch::try_new(
75+
schema,
76+
vec![
77+
generate_string_dictionary("prefix", 10, len, 1.0),
78+
generate_string_dictionary("prefix", 10, len, 0.5),
79+
generate_string_dictionary("prefix", 100, len, 1.0),
80+
generate_string_dictionary("prefix", 100, len, 0.5),
81+
generate_string_dictionary("prefix", 1000, len, 1.0),
82+
generate_string_dictionary("prefix", 1000, len, 0.5),
83+
generate_strings(0..100, len, 1.0),
84+
generate_strings(0..100, len, 0.5),
85+
generate_primitive::<Int64Type>(len, 1.0, -2000..2000),
86+
generate_primitive::<Int64Type>(len, 0.5, -2000..2000),
87+
generate_primitive::<Float64Type>(len, 1.0, -1000.0..1000.0),
88+
generate_primitive::<Float64Type>(len, 0.5, -1000.0..1000.0),
89+
],
90+
)
91+
.unwrap()
92+
}
93+
94+
fn generate_string_dictionary(
95+
prefix: &str,
96+
cardinality: usize,
97+
len: usize,
98+
valid_percent: f64,
99+
) -> ArrayRef {
100+
let mut rng = thread_rng();
101+
let strings: Vec<_> = (0..cardinality)
102+
.map(|x| format!("{}#{}", prefix, x))
103+
.collect();
104+
105+
Arc::new(DictionaryArray::<Int32Type>::from_iter((0..len).map(
106+
|_| {
107+
rng.gen_bool(valid_percent)
108+
.then(|| strings[rng.gen_range(0..cardinality)].as_str())
109+
},
110+
)))
111+
}
112+
113+
fn generate_strings(
114+
string_length_range: Range<usize>,
115+
len: usize,
116+
valid_percent: f64,
117+
) -> ArrayRef {
118+
let mut rng = thread_rng();
119+
Arc::new(StringArray::from_iter((0..len).map(|_| {
120+
rng.gen_bool(valid_percent).then(|| {
121+
let string_len = rng.gen_range(string_length_range.clone());
122+
(0..string_len)
123+
.map(|_| char::from(rng.sample(Alphanumeric)))
124+
.collect::<String>()
125+
})
126+
})))
127+
}
128+
129+
fn generate_primitive<T>(
130+
len: usize,
131+
valid_percent: f64,
132+
range: Range<T::Native>,
133+
) -> ArrayRef
134+
where
135+
T: ArrowPrimitiveType,
136+
T::Native: SampleUniform,
137+
{
138+
let mut rng = thread_rng();
139+
Arc::new(PrimitiveArray::<T>::from_iter((0..len).map(|_| {
140+
rng.gen_bool(valid_percent)
141+
.then(|| rng.gen_range(range.clone()))
142+
})))
143+
}
144+
145+
fn generate_file() -> NamedTempFile {
146+
let now = Instant::now();
147+
let named_file = tempfile::Builder::new()
148+
.prefix("parquet_query_sql")
149+
.suffix(".parquet")
150+
.tempfile()
151+
.unwrap();
152+
153+
println!("Generating parquet file - {}", named_file.path().display());
154+
let schema = schema();
155+
156+
let properties = WriterProperties::builder()
157+
.set_writer_version(WriterVersion::PARQUET_2_0)
158+
.set_max_row_group_size(ROW_GROUP_SIZE)
159+
.build();
160+
161+
let file = named_file.as_file().try_clone().unwrap();
162+
let mut writer = ArrowWriter::try_new(file, schema, Some(properties)).unwrap();
163+
164+
for _ in 0..NUM_BATCHES {
165+
let batch = generate_batch();
166+
writer.write(&batch).unwrap();
167+
}
168+
169+
let metadata = writer.close().unwrap();
170+
assert_eq!(
171+
metadata.num_rows as usize,
172+
WRITE_RECORD_BATCH_SIZE * NUM_BATCHES
173+
);
174+
assert_eq!(metadata.row_groups.len(), EXPECTED_ROW_GROUPS);
175+
176+
println!(
177+
"Generated parquet file in {} seconds",
178+
now.elapsed().as_secs_f32()
179+
);
180+
181+
named_file
182+
}
183+
184+
fn criterion_benchmark(c: &mut Criterion) {
185+
let (file_path, temp_file) = match std::env::var("PARQUET_FILE") {
186+
Ok(file) => (file, None),
187+
Err(_) => {
188+
let temp_file = generate_file();
189+
(temp_file.path().display().to_string(), Some(temp_file))
190+
}
191+
};
192+
193+
assert!(Path::new(&file_path).exists(), "path not found");
194+
println!("Using parquet file {}", file_path);
195+
196+
let mut context = ExecutionContext::new();
197+
198+
let rt = tokio::runtime::Builder::new_multi_thread().build().unwrap();
199+
rt.block_on(context.register_parquet("t", file_path.as_str()))
200+
.unwrap();
201+
202+
// We read the queries from a file so they can be changed without recompiling the benchmark
203+
let mut queries_file = File::open("benches/parquet_query_sql.sql").unwrap();
204+
let mut queries = String::new();
205+
queries_file.read_to_string(&mut queries).unwrap();
206+
207+
for query in queries.split(';') {
208+
let query = query.trim();
209+
210+
// Remove comment lines
211+
let query: Vec<_> = query.split('\n').filter(|x| !x.starts_with("--")).collect();
212+
let query = query.join(" ");
213+
214+
// Ignore blank lines
215+
if query.is_empty() {
216+
continue;
217+
}
218+
219+
let query = query.as_str();
220+
c.bench_function(query, |b| {
221+
b.iter(|| {
222+
let mut context = context.clone();
223+
rt.block_on(async move {
224+
let query = context.sql(query).await.unwrap();
225+
let mut stream = query.execute_stream().await.unwrap();
226+
while criterion::black_box(stream.next().await).is_some() {}
227+
})
228+
});
229+
});
230+
}
231+
232+
// Clean up temporary file if any
233+
std::mem::drop(temp_file);
234+
}
235+
236+
criterion_group!(benches, criterion_benchmark);
237+
criterion_main!(benches);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
-- Test scanning out dictionary columns
2+
select dict_10_optional from t;
3+
select dict_100_optional from t;
4+
select dict_1000_optional from t;
5+
6+
-- Test filtering dictionary columns
7+
select count(*) from t where dict_10_required = 'prefix#0';
8+
select count(*) from t where dict_100_required = 'prefix#0';
9+
select count(*) from t where dict_1000_required = 'prefix#0';
10+
11+
-- Test select integers
12+
select i64_optional from t where dict_10_required = 'prefix#2' and dict_1000_required = 'prefix#10';
13+
select i64_required from t where dict_10_required = 'prefix#2' and dict_1000_required = 'prefix#10';
14+
15+
-- Test select integers
16+
select string_optional from t where dict_10_required = 'prefix#1' and dict_1000_required = 'prefix#1';
17+
select string_required from t where dict_10_required = 'prefix#1' and dict_1000_required = 'prefix#1';
18+
19+
-- Test select distinct
20+
select distinct dict_10_required from t where dict_1000_optional is not NULL and i64_optional > 0;
21+
select distinct dict_10_required from t where dict_1000_optional is not NULL and i64_optional > 0;
22+
select distinct dict_10_required from t where dict_1000_optional is not NULL and i64_required > 0;
23+
select distinct dict_10_required from t where dict_1000_optional is not NULL and i64_required > 0;
24+
25+
-- Test basic aggregations
26+
select dict_10_optional, count(*) from t group by dict_10_optional;
27+
select dict_10_optional, dict_100_optional, count(*) from t group by dict_10_optional, dict_100_optional;
28+
29+
-- Test float aggregations
30+
select dict_10_optional, dict_100_optional, MIN(f64_required), MAX(f64_required), AVG(f64_required) from t group by dict_10_optional, dict_100_optional;
31+
select dict_10_optional, dict_100_optional, MIN(f64_optional), MAX(f64_optional), AVG(f64_optional) from t group by dict_10_optional, dict_100_optional;
32+
select dict_10_required, dict_100_required, MIN(f64_optional), MAX(f64_optional), AVG(f64_optional) from t group by dict_10_required, dict_100_required;

dev/release/rat_exclude_files.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ ci/*
116116
**/*.svg
117117
**/*.csv
118118
**/*.json
119+
**/*.sql
119120
venv/*
120121
testing/*
121122
target/*

0 commit comments

Comments
 (0)