Skip to content

Commit 8c0e06a

Browse files
committed
Support limit pruning
1 parent 6420a01 commit 8c0e06a

8 files changed

Lines changed: 300 additions & 21 deletions

File tree

datafusion-testing

Submodule datafusion-testing updated 285 files

datafusion/core/tests/parquet/mod.rs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ impl TestOutput {
178178
self.metric_value("page_index_rows_pruned")
179179
}
180180

181+
/// The number of row groups pruned by limit pruning
182+
fn limit_pruned_row_groups(&self) -> Option<usize> {
183+
self.metric_value("limit_pruned_row_groups")
184+
}
185+
181186
fn description(&self) -> String {
182187
format!(
183188
"Input:\n{}\nQuery:\n{}\nOutput:\n{}\nMetrics:\n{}",
@@ -191,20 +196,40 @@ impl TestOutput {
191196
/// and the appropriate scenario
192197
impl ContextWithParquet {
193198
async fn new(scenario: Scenario, unit: Unit) -> Self {
194-
Self::with_config(scenario, unit, SessionConfig::new()).await
199+
Self::with_config(scenario, unit, SessionConfig::new(), None, None).await
200+
}
201+
202+
/// Set custom schema and batches for the test
203+
pub async fn with_custom_data(
204+
scenario: Scenario,
205+
unit: Unit,
206+
schema: Arc<Schema>,
207+
batches: Vec<RecordBatch>,
208+
) -> Self {
209+
Self::with_config(
210+
scenario,
211+
unit,
212+
SessionConfig::new(),
213+
Some(schema),
214+
Some(batches),
215+
)
216+
.await
195217
}
196218

197219
async fn with_config(
198220
scenario: Scenario,
199221
unit: Unit,
200222
mut config: SessionConfig,
223+
custom_schema: Option<Arc<Schema>>,
224+
custom_batches: Option<Vec<RecordBatch>>,
201225
) -> Self {
202226
// Use a single partition for deterministic results no matter how many CPUs the host has
203227
config = config.with_target_partitions(1);
204228
let file = match unit {
205229
Unit::RowGroup(row_per_group) => {
206230
config = config.with_parquet_bloom_filter_pruning(true);
207-
make_test_file_rg(scenario, row_per_group).await
231+
make_test_file_rg(scenario, row_per_group, custom_schema, custom_batches)
232+
.await
208233
}
209234
Unit::Page(row_per_page) => {
210235
config = config.with_parquet_page_index_pruning(true);
@@ -1030,7 +1055,12 @@ fn create_data_batch(scenario: Scenario) -> Vec<RecordBatch> {
10301055
}
10311056

10321057
/// Create a test parquet file with various data types
1033-
async fn make_test_file_rg(scenario: Scenario, row_per_group: usize) -> NamedTempFile {
1058+
async fn make_test_file_rg(
1059+
scenario: Scenario,
1060+
row_per_group: usize,
1061+
custom_schema: Option<Arc<Schema>>,
1062+
custom_batches: Option<Vec<RecordBatch>>,
1063+
) -> NamedTempFile {
10341064
let mut output_file = tempfile::Builder::new()
10351065
.prefix("parquet_pruning")
10361066
.suffix(".parquet")
@@ -1043,8 +1073,14 @@ async fn make_test_file_rg(scenario: Scenario, row_per_group: usize) -> NamedTem
10431073
.set_statistics_enabled(EnabledStatistics::Page)
10441074
.build();
10451075

1046-
let batches = create_data_batch(scenario);
1047-
let schema = batches[0].schema();
1076+
let (batches, schema) =
1077+
if let (Some(schema), Some(batches)) = (custom_schema, custom_batches) {
1078+
(batches, schema)
1079+
} else {
1080+
let batches = create_data_batch(scenario);
1081+
let schema = batches[0].schema();
1082+
(batches, schema)
1083+
};
10481084

10491085
let mut writer = ArrowWriter::try_new(&mut output_file, schema, Some(props)).unwrap();
10501086

datafusion/core/tests/parquet/row_group_pruning.rs

Lines changed: 142 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818
//! This file contains an end to end test of parquet pruning. It writes
1919
//! data into a parquet file and then verifies row groups are pruned as
2020
//! expected.
21+
use std::sync::Arc;
22+
23+
use arrow::array::{ArrayRef, Int32Array, RecordBatch};
24+
use arrow_schema::{DataType, Field, Schema};
2125
use datafusion::prelude::SessionConfig;
22-
use datafusion_common::ScalarValue;
26+
use datafusion_common::{DataFusionError, ScalarValue};
2327
use itertools::Itertools;
2428

2529
use crate::parquet::Unit::RowGroup;
@@ -34,6 +38,7 @@ struct RowGroupPruningTest {
3438
expected_files_pruned_by_statistics: Option<usize>,
3539
expected_row_group_matched_by_bloom_filter: Option<usize>,
3640
expected_row_group_pruned_by_bloom_filter: Option<usize>,
41+
expected_limit_pruned_row_groups: Option<usize>,
3742
expected_rows: usize,
3843
}
3944
impl RowGroupPruningTest {
@@ -48,6 +53,7 @@ impl RowGroupPruningTest {
4853
expected_files_pruned_by_statistics: None,
4954
expected_row_group_matched_by_bloom_filter: None,
5055
expected_row_group_pruned_by_bloom_filter: None,
56+
expected_limit_pruned_row_groups: None,
5157
expected_rows: 0,
5258
}
5359
}
@@ -99,6 +105,11 @@ impl RowGroupPruningTest {
99105
self
100106
}
101107

108+
fn with_limit_pruned_row_groups(mut self, pruned_by_limit: Option<usize>) -> Self {
109+
self.expected_limit_pruned_row_groups = pruned_by_limit;
110+
self
111+
}
112+
102113
/// Set the number of expected rows from the output of this test
103114
fn with_expected_rows(mut self, rows: usize) -> Self {
104115
self.expected_rows = rows;
@@ -143,6 +154,73 @@ impl RowGroupPruningTest {
143154
self.expected_row_group_pruned_by_bloom_filter,
144155
"mismatched row_groups_pruned_bloom_filter",
145156
);
157+
assert_eq!(
158+
output.limit_pruned_row_groups(),
159+
self.expected_limit_pruned_row_groups,
160+
"mismatched limit_pruned_row_groups",
161+
);
162+
assert_eq!(
163+
output.result_rows,
164+
self.expected_rows,
165+
"Expected {} rows, got {}: {}",
166+
output.result_rows,
167+
self.expected_rows,
168+
output.description(),
169+
);
170+
}
171+
172+
// Execute the test with the current configuration
173+
async fn test_row_group_prune_with_custom_data(
174+
self,
175+
schema: Arc<Schema>,
176+
batches: Vec<RecordBatch>,
177+
) {
178+
let output = ContextWithParquet::with_custom_data(
179+
self.scenario,
180+
RowGroup(2),
181+
schema,
182+
batches,
183+
)
184+
.await
185+
.query(&self.query)
186+
.await;
187+
188+
println!("{}", output.description());
189+
assert_eq!(
190+
output.predicate_evaluation_errors(),
191+
self.expected_errors,
192+
"mismatched predicate_evaluation error"
193+
);
194+
assert_eq!(
195+
output.row_groups_matched_statistics(),
196+
self.expected_row_group_matched_by_statistics,
197+
"mismatched row_groups_matched_statistics",
198+
);
199+
assert_eq!(
200+
output.row_groups_pruned_statistics(),
201+
self.expected_row_group_pruned_by_statistics,
202+
"mismatched row_groups_pruned_statistics",
203+
);
204+
assert_eq!(
205+
output.files_ranges_pruned_statistics(),
206+
self.expected_files_pruned_by_statistics,
207+
"mismatched files_ranges_pruned_statistics",
208+
);
209+
assert_eq!(
210+
output.row_groups_matched_bloom_filter(),
211+
self.expected_row_group_matched_by_bloom_filter,
212+
"mismatched row_groups_matched_bloom_filter",
213+
);
214+
assert_eq!(
215+
output.row_groups_pruned_bloom_filter(),
216+
self.expected_row_group_pruned_by_bloom_filter,
217+
"mismatched row_groups_pruned_bloom_filter",
218+
);
219+
assert_eq!(
220+
output.limit_pruned_row_groups(),
221+
self.expected_limit_pruned_row_groups,
222+
"mismatched limit_pruned_row_groups",
223+
);
146224
assert_eq!(
147225
output.result_rows,
148226
self.expected_rows,
@@ -287,11 +365,16 @@ async fn prune_disabled() {
287365
let expected_rows = 10;
288366
let config = SessionConfig::new().with_parquet_pruning(false);
289367

290-
let output =
291-
ContextWithParquet::with_config(Scenario::Timestamps, RowGroup(5), config)
292-
.await
293-
.query(query)
294-
.await;
368+
let output = ContextWithParquet::with_config(
369+
Scenario::Timestamps,
370+
RowGroup(5),
371+
config,
372+
None,
373+
None,
374+
)
375+
.await
376+
.query(query)
377+
.await;
295378
println!("{}", output.description());
296379

297380
// This should not prune any
@@ -1634,3 +1717,56 @@ async fn test_bloom_filter_decimal_dict() {
16341717
.test_row_group_prune()
16351718
.await;
16361719
}
1720+
1721+
// Helper function to create a batch with a single Int32 column.
1722+
fn make_i32_batch(
1723+
name: &str,
1724+
values: Vec<i32>,
1725+
) -> datafusion_common::error::Result<RecordBatch> {
1726+
let schema = Arc::new(Schema::new(vec![Field::new(name, DataType::Int32, false)]));
1727+
let array: ArrayRef = Arc::new(Int32Array::from(values));
1728+
RecordBatch::try_new(schema, vec![array]).map_err(DataFusionError::from)
1729+
}
1730+
1731+
#[tokio::test]
1732+
async fn test_limit_pruning() -> datafusion_common::error::Result<()> {
1733+
// Scenario: Simple integer column, multiple row groups
1734+
// Query: SELECT c1 FROM t WHERE c1 > 0 LIMIT 2
1735+
// We expect 2 rows in total.
1736+
1737+
// Row Group 0: c1 = [1, 2] -> Fully matched, 2 rows
1738+
// Row Group 1: c1 = [3, 4] -> Fully matched, 2 rows
1739+
// Row Group 2: c1 = [5, 6] -> Fully matched, 2 rows
1740+
// Row Group 3: c1 = [-1, 0] -> Pruned by statistics, 0 rows
1741+
1742+
// If limit = 2, and RG0 is fully matched and has 2 rows, we should
1743+
// only scan RG0 and prune other row groups (RG1, RG2, RG3)
1744+
// RG3 is pruned by statistics. RG1 and RG2 are pruned by limit.
1745+
// So 3 row groups are effectively pruned due to limit pruning.
1746+
1747+
let schema = Arc::new(Schema::new(vec![Field::new("c1", DataType::Int32, false)]));
1748+
let query = "explain verbose SELECT c1 FROM t WHERE c1 > 0 LIMIT 2";
1749+
1750+
let batches = vec![
1751+
make_i32_batch("c1", vec![1, 2])?, // RG0: Fully matched, 2 rows
1752+
make_i32_batch("c1", vec![3, 4])?, // RG1: Fully matched, 2 rows
1753+
make_i32_batch("c1", vec![5, 6])?, // RG2: Fully matched, 2 rows
1754+
make_i32_batch("c1", vec![-1, 0])?, // RG3: Pruned by statistics, 0 rows
1755+
];
1756+
1757+
RowGroupPruningTest::new()
1758+
.with_scenario(Scenario::Int) // Assuming Scenario::Int can handle this data
1759+
.with_query(query)
1760+
.with_expected_errors(Some(0))
1761+
.with_expected_rows(2)
1762+
.with_pruned_files(Some(0))
1763+
.with_matched_by_bloom_filter(Some(0))
1764+
.with_pruned_by_bloom_filter(Some(0))
1765+
.with_matched_by_stats(Some(3)) // RG0, RG1, RG2 are matched by stats (c1 > 0)
1766+
.with_pruned_by_stats(Some(1)) // RG3 is pruned by stats (c1 = [-1, 0] does not satisfy c1 > 0)
1767+
// .with_limit_pruned_row_groups(Some(2)) // RG1, RG2 are pruned by limit. (RG3 is already pruned by stats)
1768+
.test_row_group_prune_with_custom_data(schema, batches)
1769+
.await;
1770+
1771+
Ok(())
1772+
}

datafusion/datasource-parquet/src/metrics.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ pub struct ParquetFileMetrics {
4848
pub row_groups_matched_bloom_filter: Count,
4949
/// Number of row groups pruned by bloom filters
5050
pub row_groups_pruned_bloom_filter: Count,
51+
/// Number of row groups pruned due to limit pruning.
52+
pub limit_pruned_row_groups: Count,
5153
/// Number of row groups whose statistics were checked and matched (not pruned)
5254
pub row_groups_matched_statistics: Count,
5355
/// Number of row groups pruned by statistics
@@ -93,6 +95,10 @@ impl ParquetFileMetrics {
9395
.with_new_label("filename", filename.to_string())
9496
.counter("row_groups_pruned_bloom_filter", partition);
9597

98+
let limit_pruned_row_groups = MetricBuilder::new(metrics)
99+
.with_new_label("filename", filename.to_string())
100+
.counter("limit_pruned_row_groups", partition);
101+
96102
let row_groups_matched_statistics = MetricBuilder::new(metrics)
97103
.with_new_label("filename", filename.to_string())
98104
.counter("row_groups_matched_statistics", partition);
@@ -147,6 +153,7 @@ impl ParquetFileMetrics {
147153
row_groups_pruned_bloom_filter,
148154
row_groups_matched_statistics,
149155
row_groups_pruned_statistics,
156+
limit_pruned_row_groups,
150157
bytes_scanned,
151158
pushdown_rows_pruned,
152159
pushdown_rows_matched,

datafusion/datasource-parquet/src/opener.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,12 @@ impl FileOpener for ParquetOpener {
373373
}
374374
}
375375

376-
let mut access_plan = row_groups.build();
376+
// Prune by limit
377+
if let Some(limit) = limit {
378+
row_groups.prune_by_limit(limit, rg_metadata, &file_metrics);
379+
}
377380

381+
let mut access_plan = row_groups.build();
378382
// page index pruning: if all data on individual pages can
379383
// be ruled using page metadata, rows from other columns
380384
// with that range can be skipped as well

0 commit comments

Comments
 (0)