Skip to content

Commit 06d618f

Browse files
committed
bench: add parquet bloom filter pruning benchmarks
1 parent 910e8ee commit 06d618f

2 files changed

Lines changed: 232 additions & 4 deletions

File tree

‎datafusion/core/Cargo.toml‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,6 @@ name = "math_query_sql"
245245
harness = false
246246
name = "filter_query_sql"
247247

248-
[[bench]]
249-
harness = false
250-
name = "in_list_rewrite"
251-
252248
[[bench]]
253249
harness = false
254250
name = "struct_query_sql"
@@ -274,6 +270,11 @@ harness = false
274270
name = "parquet_query_sql"
275271
required-features = ["parquet"]
276272

273+
[[bench]]
274+
harness = false
275+
name = "parquet_bloom_filter_pruning"
276+
required-features = ["parquet"]
277+
277278
[[bench]]
278279
harness = false
279280
name = "parquet_struct_query"
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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 for Parquet row-group pruning with Bloom filters on several
19+
//! predicate columns.
20+
//!
21+
//! The file has [`ROW_GROUPS`] row groups and [`MAX_COLUMNS`] `Int32` columns,
22+
//! all with Bloom filters and without min/max statistics, so only Bloom
23+
//! filters can prune. Row 0 of every row group holds `0` in every column, and
24+
//! `-1` appears nowhere. Each query is a conjunction of equality predicates on
25+
//! the first `K` columns:
26+
//!
27+
//! - `reject_first`: `c0 = -1 AND c1 = 0 AND ...`; the first column's filter
28+
//! can reject every row group.
29+
//! - `reject_last`: `c0 = 0 AND ... AND c{K-1} = -1`; only the last column's
30+
//! filter can reject, so every filter is loaded before pruning.
31+
//! - `survive_all`: `c0 = 0 AND ... AND c{K-1} = 0`; no filter can reject, so
32+
//! every filter is loaded and every row group is scanned.
33+
//!
34+
//! Each query runs against an in-memory store and against a store that adds
35+
//! [`STORE_LATENCY`] to every request, which approximates remote storage.
36+
//!
37+
//! Run with `cargo bench -p datafusion --features parquet --bench parquet_bloom_filter_pruning`.
38+
39+
use std::sync::Arc;
40+
use std::time::Duration;
41+
42+
use arrow::array::{ArrayRef, Int32Array, RecordBatch};
43+
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
44+
use criterion::{BenchmarkId, Criterion, SamplingMode, criterion_group, criterion_main};
45+
use datafusion::datasource::file_format::parquet::ParquetFormat;
46+
use datafusion::datasource::listing::ListingOptions;
47+
use datafusion::prelude::{SessionConfig, SessionContext};
48+
use datafusion_execution::runtime_env::RuntimeEnv;
49+
use object_store::memory::InMemory;
50+
use object_store::path::Path;
51+
use object_store::throttle::{ThrottleConfig, ThrottledStore};
52+
use object_store::{ObjectStore, ObjectStoreExt};
53+
use parquet::arrow::ArrowWriter;
54+
use parquet::file::properties::{EnabledStatistics, WriterProperties};
55+
use tokio::runtime::Runtime;
56+
use url::Url;
57+
58+
const ROW_GROUPS: usize = 64;
59+
const ROW_GROUP_ROWS: usize = 1024;
60+
const MAX_COLUMNS: usize = 32;
61+
const COLUMN_COUNTS: [usize; 3] = [2, 8, 32];
62+
const STORE_LATENCY: Duration = Duration::from_micros(200);
63+
const PRESENT: i32 = 0;
64+
const ABSENT: i32 = -1;
65+
66+
fn schema() -> SchemaRef {
67+
Arc::new(Schema::new(
68+
(0..MAX_COLUMNS)
69+
.map(|i| Field::new(format!("c{i}"), DataType::Int32, false))
70+
.collect::<Vec<_>>(),
71+
))
72+
}
73+
74+
fn create_parquet_file() -> Vec<u8> {
75+
let schema = schema();
76+
let props = WriterProperties::builder()
77+
.set_max_row_group_row_count(Some(ROW_GROUP_ROWS))
78+
.set_statistics_enabled(EnabledStatistics::None)
79+
.set_bloom_filter_enabled(true)
80+
.set_bloom_filter_max_ndv(ROW_GROUP_ROWS as u64)
81+
.set_bloom_filter_fpp(0.01)
82+
.build();
83+
84+
let mut data = Vec::new();
85+
let mut writer =
86+
ArrowWriter::try_new(&mut data, Arc::clone(&schema), Some(props)).unwrap();
87+
for rg in 0..ROW_GROUPS {
88+
// Row 0 is PRESENT in every row group; other values are unique and positive.
89+
let values: Int32Array = (0..ROW_GROUP_ROWS)
90+
.map(|row| {
91+
if row == 0 {
92+
PRESENT
93+
} else {
94+
(rg * ROW_GROUP_ROWS + row) as i32
95+
}
96+
})
97+
.collect();
98+
let column: ArrayRef = Arc::new(values);
99+
let columns = vec![column; MAX_COLUMNS];
100+
let batch = RecordBatch::try_new(Arc::clone(&schema), columns).unwrap();
101+
writer.write(&batch).unwrap();
102+
}
103+
writer.close().unwrap();
104+
data
105+
}
106+
107+
fn make_store_slow(object_store: InMemory) -> Arc<dyn ObjectStore> {
108+
let config = ThrottleConfig {
109+
wait_get_per_call: STORE_LATENCY,
110+
..Default::default()
111+
};
112+
Arc::new(ThrottledStore::new(object_store, config))
113+
}
114+
115+
async fn setup_context() -> SessionContext {
116+
let data = create_parquet_file();
117+
let stores: [(&str, Arc<dyn ObjectStore>); 2] = [
118+
("memory", Arc::new(InMemory::new())),
119+
("slow", make_store_slow(InMemory::new())),
120+
];
121+
122+
// One partition keeps the Bloom filter reads for the file in one opener.
123+
let config = SessionConfig::new().with_target_partitions(1);
124+
let rt = Arc::new(RuntimeEnv::default());
125+
for (name, store) in &stores {
126+
store
127+
.put(&Path::from("t/data.parquet"), data.clone().into())
128+
.await
129+
.unwrap();
130+
rt.register_object_store(
131+
&Url::parse(&format!("{name}://bucket")).unwrap(),
132+
Arc::clone(store),
133+
);
134+
}
135+
let ctx = SessionContext::new_with_config_rt(config, rt);
136+
137+
for (name, _) in &stores {
138+
let options = ListingOptions::new(Arc::new(ParquetFormat::default()));
139+
ctx.register_listing_table(
140+
*name,
141+
format!("{name}://bucket/t/"),
142+
options,
143+
Some(schema()),
144+
None,
145+
)
146+
.await
147+
.unwrap();
148+
}
149+
ctx
150+
}
151+
152+
/// Builds the query and its expected row count.
153+
fn query(table: &str, case: &str, columns: usize) -> (String, usize) {
154+
let predicate = (0..columns)
155+
.map(|i| {
156+
let rejecting = match case {
157+
"reject_first" => i == 0,
158+
"reject_last" => i == columns - 1,
159+
"survive_all" => false,
160+
_ => unreachable!(),
161+
};
162+
let value = if rejecting { ABSENT } else { PRESENT };
163+
format!("c{i} = {value}")
164+
})
165+
.collect::<Vec<_>>()
166+
.join(" AND ");
167+
let expected_rows = if case == "survive_all" { ROW_GROUPS } else { 0 };
168+
(
169+
format!("SELECT c0 FROM {table} WHERE {predicate}"),
170+
expected_rows,
171+
)
172+
}
173+
174+
async fn run(ctx: &SessionContext, sql: &str, expected_rows: usize) {
175+
let batches = ctx.sql(sql).await.unwrap().collect().await.unwrap();
176+
let rows: usize = batches.iter().map(|b| b.num_rows()).sum();
177+
assert_eq!(rows, expected_rows);
178+
}
179+
180+
/// Prints the Bloom filter pruning metric once so a run shows whether Bloom
181+
/// filters actually pruned.
182+
async fn print_bloom_metrics(ctx: &SessionContext, sql: &str) {
183+
let batches = ctx
184+
.sql(&format!("EXPLAIN ANALYZE {sql}"))
185+
.await
186+
.unwrap()
187+
.collect()
188+
.await
189+
.unwrap();
190+
let plan = arrow::util::pretty::pretty_format_batches(&batches)
191+
.unwrap()
192+
.to_string();
193+
let metric = plan
194+
.split([',', '['])
195+
.find(|s| s.contains("row_groups_pruned_bloom_filter"))
196+
.unwrap_or("row_groups_pruned_bloom_filter not reported");
197+
println!("{sql}\n {}", metric.trim());
198+
}
199+
200+
fn criterion_benchmark(c: &mut Criterion) {
201+
let rt = Runtime::new().unwrap();
202+
let ctx = rt.block_on(setup_context());
203+
204+
for case in ["reject_first", "reject_last", "survive_all"] {
205+
for table in ["memory", "slow"] {
206+
let mut group =
207+
c.benchmark_group(format!("parquet_bloom_filter_pruning/{table}/{case}"));
208+
group.sampling_mode(SamplingMode::Flat);
209+
group.sample_size(10);
210+
for columns in COLUMN_COUNTS {
211+
let (sql, expected_rows) = query(table, case, columns);
212+
rt.block_on(print_bloom_metrics(&ctx, &sql));
213+
group.bench_with_input(
214+
BenchmarkId::new("columns", columns),
215+
&sql,
216+
|b, sql| {
217+
b.to_async(&rt).iter(|| run(&ctx, sql, expected_rows));
218+
},
219+
);
220+
}
221+
group.finish();
222+
}
223+
}
224+
}
225+
226+
criterion_group!(benches, criterion_benchmark);
227+
criterion_main!(benches);

0 commit comments

Comments
 (0)