Skip to content

Commit f3b1216

Browse files
authored
fix: Correct the number of pruned/matched Parquet pages (apache#22031)
## Which issue does this PR close? - Closes apache#22030. ## Rationale for this change Show the correct metrics in the execution plan. ## What changes are included in this PR? - Keep track of the pages matched by each pruning predicate to avoid double counting them. - Added unit test and updated existing ones. ## Are these changes tested? Yes. ## Are there any user-facing changes? No.
1 parent 2a14a93 commit f3b1216

4 files changed

Lines changed: 99 additions & 15 deletions

File tree

datafusion/core/src/datasource/physical_plan/parquet.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,73 @@ mod tests {
18001800
assert_eq!(page_index_pages_matched, 1);
18011801
}
18021802

1803+
#[tokio::test]
1804+
async fn parquet_page_index_exec_metrics_multiple_predicates() {
1805+
let c1: ArrayRef =
1806+
Arc::new(Int32Array::from(vec![Some(1), Some(2), Some(3), Some(4)]));
1807+
let c2: ArrayRef = Arc::new(Int32Array::from(vec![
1808+
Some(11),
1809+
Some(12),
1810+
Some(13),
1811+
Some(14),
1812+
]));
1813+
let batch = create_batch(vec![("a", c1), ("b", c2)]);
1814+
1815+
// matches the last page
1816+
let filter = col("a").gt_eq(lit(3)).and(col("b").lt_eq(lit(13)));
1817+
let rt = RoundTrip::new()
1818+
.with_predicate(filter)
1819+
.with_page_index_predicate()
1820+
.round_trip(vec![batch.clone()])
1821+
.await;
1822+
let metrics = rt.parquet_exec.metrics().unwrap();
1823+
1824+
let (page_index_rows_pruned, page_index_rows_matched) =
1825+
get_pruning_metric(&metrics, "page_index_rows_pruned");
1826+
assert_eq!(page_index_rows_pruned, 2);
1827+
assert_eq!(page_index_rows_matched, 2);
1828+
let (page_index_pages_pruned, page_index_pages_matched) =
1829+
get_pruning_metric(&metrics, "page_index_pages_pruned");
1830+
assert_eq!(page_index_pages_pruned, 1);
1831+
assert_eq!(page_index_pages_matched, 1);
1832+
1833+
// matches no pages
1834+
let filter = col("a").gt_eq(lit(3)).and(col("b").lt_eq(lit(12)));
1835+
let rt = RoundTrip::new()
1836+
.with_predicate(filter)
1837+
.with_page_index_predicate()
1838+
.round_trip(vec![batch.clone()])
1839+
.await;
1840+
let metrics = rt.parquet_exec.metrics().unwrap();
1841+
1842+
let (page_index_rows_pruned, page_index_rows_matched) =
1843+
get_pruning_metric(&metrics, "page_index_rows_pruned");
1844+
assert_eq!(page_index_rows_pruned, 4);
1845+
assert_eq!(page_index_rows_matched, 0);
1846+
let (page_index_pages_pruned, page_index_pages_matched) =
1847+
get_pruning_metric(&metrics, "page_index_pages_pruned");
1848+
assert_eq!(page_index_pages_pruned, 2);
1849+
assert_eq!(page_index_pages_matched, 0);
1850+
1851+
// matches both pages
1852+
let filter = col("a").gt_eq(lit(2)).and(col("b").lt_eq(lit(13)));
1853+
let rt = RoundTrip::new()
1854+
.with_predicate(filter)
1855+
.with_page_index_predicate()
1856+
.round_trip(vec![batch.clone()])
1857+
.await;
1858+
let metrics = rt.parquet_exec.metrics().unwrap();
1859+
1860+
let (page_index_rows_pruned, page_index_rows_matched) =
1861+
get_pruning_metric(&metrics, "page_index_rows_pruned");
1862+
assert_eq!(page_index_rows_pruned, 0);
1863+
assert_eq!(page_index_rows_matched, 4);
1864+
let (page_index_pages_pruned, page_index_pages_matched) =
1865+
get_pruning_metric(&metrics, "page_index_pages_pruned");
1866+
assert_eq!(page_index_pages_pruned, 0);
1867+
assert_eq!(page_index_pages_matched, 2);
1868+
}
1869+
18031870
/// Returns a string array with contents:
18041871
/// "[Foo, null, bar, bar, bar, bar, zzz]"
18051872
fn string_batch() -> RecordBatch {

datafusion/datasource-parquet/src/page_filter.rs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ impl PagePruningAccessPlanFilter {
199199
for row_group_index in row_group_indexes {
200200
// The selection for this particular row group
201201
let mut overall_selection = None;
202+
let mut total_pages_in_group = 0;
203+
// stores the indexes of the matched pages
204+
let mut matched_pages_in_group: Option<HashSet<usize>> = None;
205+
202206
for predicate in page_index_predicates {
203207
let column = predicate
204208
.required_columns()
@@ -230,19 +234,31 @@ impl PagePruningAccessPlanFilter {
230234
file_metrics,
231235
);
232236

233-
let Some((selection, total_pages, matched_pages)) = selection else {
237+
let Some((selection, pages)) = selection else {
234238
trace!("No pages pruned in prune_pages_in_one_row_group");
235239
continue;
236240
};
237-
total_pages_select += matched_pages;
238-
total_pages_skip += total_pages - matched_pages;
239241

240242
debug!(
241243
"Use filter and page index to create RowSelection {:?} from predicate: {:?}",
242244
&selection,
243245
predicate.predicate_expr(),
244246
);
245247

248+
total_pages_in_group = pages.len();
249+
let matched_pages_indexes: HashSet<_> = pages
250+
.into_iter()
251+
.enumerate()
252+
.filter(|x| x.1)
253+
.map(|x| x.0)
254+
.collect();
255+
if let Some(ref mut m) = matched_pages_in_group {
256+
// only keep pages that also matched in the previous predicate(s)
257+
m.retain(|x| matched_pages_indexes.contains(x));
258+
} else {
259+
matched_pages_in_group = Some(matched_pages_indexes);
260+
}
261+
246262
overall_selection = update_selection(overall_selection, selection);
247263

248264
// if the overall selection has ruled out all rows, no need to
@@ -278,6 +294,10 @@ impl PagePruningAccessPlanFilter {
278294
);
279295
}
280296
}
297+
298+
let pages_matched = matched_pages_in_group.map_or(0, |m| m.len());
299+
total_pages_select += pages_matched;
300+
total_pages_skip += total_pages_in_group - pages_matched;
281301
}
282302

283303
file_metrics.page_index_rows_pruned.add_pruned(total_skip);
@@ -309,8 +329,8 @@ fn update_selection(
309329
}
310330
}
311331

312-
/// Returns a [`RowSelection`] for the rows in this row group to scan, in addition to the number of
313-
/// total and matched pages.
332+
/// Returns a [`RowSelection`] for the rows in this row group to scan, in addition to a vec of
333+
/// booleans that state if each page was matched (true) or not (false).
314334
///
315335
/// This Row Selection is formed from the page index and the predicate skips row
316336
/// ranges that can be ruled out based on the predicate.
@@ -323,7 +343,7 @@ fn prune_pages_in_one_row_group(
323343
converter: StatisticsConverter<'_>,
324344
parquet_metadata: &ParquetMetaData,
325345
metrics: &ParquetFileMetrics,
326-
) -> Option<(RowSelection, usize, usize)> {
346+
) -> Option<(RowSelection, Vec<bool>)> {
327347
let pruning_stats =
328348
PagesPruningStatistics::try_new(row_group_index, converter, parquet_metadata)?;
329349

@@ -376,10 +396,7 @@ fn prune_pages_in_one_row_group(
376396
};
377397
vec.push(selector);
378398

379-
let total_pages = values.len();
380-
let matched_pages = values.iter().filter(|v| **v).count();
381-
382-
Some((RowSelection::from(vec), total_pages, matched_pages))
399+
Some((RowSelection::from(vec), values))
383400
}
384401

385402
/// Implement [`PruningStatistics`] for one column's PageIndex (column_index + offset_index)

datafusion/sqllogictest/test_files/explain_analyze.slt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ explain analyze select * from cat_tracking where species > 'M' AND s >= 50 order
247247
----
248248
Plan with Metrics
249249
01)SortExec: TopK(fetch=3), expr=[species@0 ASC NULLS LAST], preserve_partitioning=[false], filter=[species@0 < Nlpine Sheep], metrics=[output_rows=3]
250-
02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/explain_analyze/data.parquet]]}, projection=[species, s], file_type=parquet, predicate=species@0 > M AND s@1 >= 50 AND DynamicFilter [ species@0 < Nlpine Sheep ], pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50 AND species_null_count@1 != row_count@2 AND species_min@5 < Nlpine Sheep, required_guarantees=[], metrics=[output_rows=3, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=6 total → 6 matched, limit_pruned_row_groups=0 total → 0 matched, scan_efficiency_ratio=22.13% (521/2.35 K)]
250+
02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/explain_analyze/data.parquet]]}, projection=[species, s], file_type=parquet, predicate=species@0 > M AND s@1 >= 50 AND DynamicFilter [ species@0 < Nlpine Sheep ], pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50 AND species_null_count@1 != row_count@2 AND species_min@5 < Nlpine Sheep, required_guarantees=[], metrics=[output_rows=3, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=3 total → 3 matched, limit_pruned_row_groups=0 total → 0 matched, scan_efficiency_ratio=22.13% (521/2.35 K)]
251251

252252
statement ok
253253
reset datafusion.explain.analyze_categories;
@@ -262,7 +262,7 @@ explain analyze select * from cat_tracking where species > 'M' AND s >= 50 order
262262
----
263263
Plan with Metrics
264264
01)SortExec: TopK(fetch=3), expr=[species@0 ASC NULLS LAST], preserve_partitioning=[false], filter=[species@0 < Nlpine Sheep], metrics=[output_rows=3, output_bytes=<slt:ignore>]
265-
02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/explain_analyze/data.parquet]]}, projection=[species, s], file_type=parquet, predicate=species@0 > M AND s@1 >= 50 AND DynamicFilter [ species@0 < Nlpine Sheep ], pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50 AND species_null_count@1 != row_count@2 AND species_min@5 < Nlpine Sheep, required_guarantees=[], metrics=[output_rows=3, output_bytes=<slt:ignore>, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=6 total → 6 matched, limit_pruned_row_groups=0 total → 0 matched, bytes_scanned=<slt:ignore>, scan_efficiency_ratio=<slt:ignore>]
265+
02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/explain_analyze/data.parquet]]}, projection=[species, s], file_type=parquet, predicate=species@0 > M AND s@1 >= 50 AND DynamicFilter [ species@0 < Nlpine Sheep ], pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50 AND species_null_count@1 != row_count@2 AND species_min@5 < Nlpine Sheep, required_guarantees=[], metrics=[output_rows=3, output_bytes=<slt:ignore>, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=3 total → 3 matched, limit_pruned_row_groups=0 total → 0 matched, bytes_scanned=<slt:ignore>, scan_efficiency_ratio=<slt:ignore>]
266266

267267
statement ok
268268
reset datafusion.explain.analyze_categories;
@@ -277,7 +277,7 @@ explain analyze select * from cat_tracking where species > 'M' AND s >= 50 order
277277
----
278278
Plan with Metrics
279279
01)SortExec: TopK(fetch=3), expr=[species@0 ASC NULLS LAST], preserve_partitioning=[false], filter=[species@0 < Nlpine Sheep], metrics=[output_rows=3, output_bytes=<slt:ignore>]
280-
02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/explain_analyze/data.parquet]]}, projection=[species, s], file_type=parquet, predicate=species@0 > M AND s@1 >= 50 AND DynamicFilter [ species@0 < Nlpine Sheep ], pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50 AND species_null_count@1 != row_count@2 AND species_min@5 < Nlpine Sheep, required_guarantees=[], metrics=[output_rows=3, output_bytes=<slt:ignore>, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=6 total → 6 matched, limit_pruned_row_groups=0 total → 0 matched, bytes_scanned=<slt:ignore>, scan_efficiency_ratio=<slt:ignore>]
280+
02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/explain_analyze/data.parquet]]}, projection=[species, s], file_type=parquet, predicate=species@0 > M AND s@1 >= 50 AND DynamicFilter [ species@0 < Nlpine Sheep ], pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50 AND species_null_count@1 != row_count@2 AND species_min@5 < Nlpine Sheep, required_guarantees=[], metrics=[output_rows=3, output_bytes=<slt:ignore>, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=3 total → 3 matched, limit_pruned_row_groups=0 total → 0 matched, bytes_scanned=<slt:ignore>, scan_efficiency_ratio=<slt:ignore>]
281281

282282
statement ok
283283
reset datafusion.explain.analyze_categories;

datafusion/sqllogictest/test_files/limit_pruning.slt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ set datafusion.explain.analyze_level = summary;
6363
query TT
6464
explain analyze select * from tracking_data where species > 'M' AND s >= 50 limit 3;
6565
----
66-
Plan with Metrics DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/limit_pruning/data.parquet]]}, projection=[species, s], limit=3, file_type=parquet, predicate=species@0 > M AND s@1 >= 50, pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50, required_guarantees=[], metrics=[output_rows=3, elapsed_compute=<slt:ignore>, output_bytes=<slt:ignore>, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=2 total → 2 matched, limit_pruned_row_groups=2 total → 0 matched, bytes_scanned=<slt:ignore>, metadata_load_time=<slt:ignore>, scan_efficiency_ratio=<slt:ignore> (171/2.35 K)]
66+
Plan with Metrics DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/limit_pruning/data.parquet]]}, projection=[species, s], limit=3, file_type=parquet, predicate=species@0 > M AND s@1 >= 50, pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50, required_guarantees=[], metrics=[output_rows=3, elapsed_compute=<slt:ignore>, output_bytes=<slt:ignore>, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=1 total → 1 matched, limit_pruned_row_groups=2 total → 0 matched, bytes_scanned=<slt:ignore>, metadata_load_time=<slt:ignore>, scan_efficiency_ratio=<slt:ignore> (171/2.35 K)]
6767

6868
# limit_pruned_row_groups=0 total → 0 matched
6969
# because of order by, scan needs to preserve sort, so limit pruning is disabled
@@ -72,7 +72,7 @@ explain analyze select * from tracking_data where species > 'M' AND s >= 50 orde
7272
----
7373
Plan with Metrics
7474
01)SortExec: TopK(fetch=3), expr=[species@0 ASC NULLS LAST], preserve_partitioning=[false], filter=[species@0 < Nlpine Sheep], metrics=[output_rows=3, elapsed_compute=<slt:ignore>, output_bytes=<slt:ignore>]
75-
02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/limit_pruning/data.parquet]]}, projection=[species, s], file_type=parquet, predicate=species@0 > M AND s@1 >= 50 AND DynamicFilter [ species@0 < Nlpine Sheep ], pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50 AND species_null_count@1 != row_count@2 AND species_min@5 < Nlpine Sheep, required_guarantees=[], metrics=[output_rows=3, elapsed_compute=<slt:ignore>, output_bytes=<slt:ignore>, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=6 total → 6 matched, limit_pruned_row_groups=0 total → 0 matched, bytes_scanned=<slt:ignore>, metadata_load_time=<slt:ignore>, scan_efficiency_ratio=<slt:ignore> (521/2.35 K)]
75+
02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/limit_pruning/data.parquet]]}, projection=[species, s], file_type=parquet, predicate=species@0 > M AND s@1 >= 50 AND DynamicFilter [ species@0 < Nlpine Sheep ], pruning_predicate=species_null_count@1 != row_count@2 AND species_max@0 > M AND s_null_count@4 != row_count@2 AND s_max@3 >= 50 AND species_null_count@1 != row_count@2 AND species_min@5 < Nlpine Sheep, required_guarantees=[], metrics=[output_rows=3, elapsed_compute=<slt:ignore>, output_bytes=<slt:ignore>, files_ranges_pruned_statistics=1 total → 1 matched, row_groups_pruned_statistics=4 total → 3 matched -> 1 fully matched, row_groups_pruned_bloom_filter=3 total → 3 matched, page_index_pages_pruned=3 total → 3 matched, limit_pruned_row_groups=0 total → 0 matched, bytes_scanned=<slt:ignore>, metadata_load_time=<slt:ignore>, scan_efficiency_ratio=<slt:ignore> (521/2.35 K)]
7676

7777
statement ok
7878
drop table tracking_data;

0 commit comments

Comments
 (0)