-
Notifications
You must be signed in to change notification settings - Fork 2.3k
fix(parquet): don't runtime-prune row groups while a page-index RowSelection is live (#24355) #24359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(parquet): don't runtime-prune row groups while a page-index RowSelection is live (#24355) #24359
Changes from all commits
7dd27a6
d5065af
29be806
34d7972
1370d2b
9641904
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1435,7 +1435,7 @@ impl RowGroupsPrunedParquetOpen { | |
| prepared.virtual_state.as_deref(), | ||
| )?; | ||
|
|
||
| let (decoder, rg_plan) = { | ||
| let (decoder, rg_plan, has_row_selection) = { | ||
| let pushdown_predicate = prepared | ||
| .pushdown_filters | ||
| .then_some(prepared.predicate.as_ref()) | ||
|
|
@@ -1464,6 +1464,18 @@ impl RowGroupsPrunedParquetOpen { | |
| }; | ||
|
|
||
| let prepared_access_plan = prepare_access_plan(access_plan)?; | ||
| // #24355: a row selection (from page-index pruning, or an externally | ||
| // supplied `ParquetRowSelection`) is carried by the decoder as one | ||
| // flat selection over the concatenation of the remaining row groups. | ||
| // The runtime pruner's `into_builder().with_row_groups(...)` rebuild | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. another good place to leave a link to the proper ticket fix |
||
| // drops row groups without slicing that selection to match, so record | ||
| // whether a selection is present and disable runtime pruning below | ||
| // when it is (mirroring `reorder_by_statistics`, which also bails when | ||
| // a row selection is present). The proper fix that keeps pruning | ||
| // under a live selection is tracked in | ||
| // https://github.com/apache/arrow-rs/issues/10624 / | ||
| // https://github.com/apache/datafusion/issues/24358. | ||
| let has_row_selection = prepared_access_plan.row_selection.is_some(); | ||
| let rg_plan: VecDeque<RgPlanEntry> = prepared_access_plan | ||
| .row_group_indexes | ||
| .iter() | ||
|
|
@@ -1482,7 +1494,7 @@ impl RowGroupsPrunedParquetOpen { | |
| } | ||
| } | ||
|
|
||
| (builder.build()?, rg_plan) | ||
| (builder.build()?, rg_plan, has_row_selection) | ||
| }; | ||
|
|
||
| let predicate_cache_inner_records = | ||
|
|
@@ -1504,24 +1516,30 @@ impl RowGroupsPrunedParquetOpen { | |
| // via the `DynamicFilterTracker` watch channel (#22460), so detecting | ||
| // a threshold change is a single atomic load — not a tree walk per | ||
| // RG check. | ||
| let row_group_pruner = match (&prepared.predicate, rg_plan.len() > 1) { | ||
| (Some(predicate), true) | ||
| if matches!( | ||
| DynamicFilterTracking::classify(predicate), | ||
| DynamicFilterTracking::Watching(_) | ||
| ) => | ||
| { | ||
| Some(RowGroupPruner::new( | ||
| Arc::clone(predicate), | ||
| Arc::clone(&prepared.physical_file_schema), | ||
| Arc::clone(reader_metadata.metadata()), | ||
| prepared.predicate_creation_errors.clone(), | ||
| prepared.file_metrics.predicate_evaluation_errors.clone(), | ||
| prepared.max_in_list_size, | ||
| )) | ||
| } | ||
| _ => None, | ||
| }; | ||
| // Also disabled when a row selection is live (#24355) — page-index | ||
| // pruning is the common source: the pruner rebuilds the decoder via | ||
| // `with_row_groups(...)`, which drops row groups without slicing the | ||
| // carried selection to match, so pruning under a live selection returns | ||
| // wrong results. Decline to prune in that case. | ||
| let row_group_pruner = | ||
| match (&prepared.predicate, rg_plan.len() > 1, has_row_selection) { | ||
| (Some(predicate), true, false) | ||
| if matches!( | ||
| DynamicFilterTracking::classify(predicate), | ||
| DynamicFilterTracking::Watching(_) | ||
| ) => | ||
| { | ||
| Some(RowGroupPruner::new( | ||
| Arc::clone(predicate), | ||
| Arc::clone(&prepared.physical_file_schema), | ||
| Arc::clone(reader_metadata.metadata()), | ||
| prepared.predicate_creation_errors.clone(), | ||
| prepared.file_metrics.predicate_evaluation_errors.clone(), | ||
| prepared.max_in_list_size, | ||
| )) | ||
| } | ||
| _ => None, | ||
| }; | ||
| let row_groups_pruned_dynamic = prepared | ||
| .file_metrics | ||
| .row_groups_pruned_dynamic_filter | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -194,3 +194,96 @@ RESET datafusion.optimizer.enable_dynamic_filter_pushdown; | |
|
|
||
| statement ok | ||
| RESET datafusion.optimizer.enable_topk_dynamic_filter_pushdown; | ||
|
|
||
| # Regression test for a scan where two pruning mechanisms are live at once: | ||
| # page-index pruning leaves an intra-row-group `RowSelection`, and a TopK | ||
| # dynamic filter prunes row groups at runtime. The property under test is that | ||
| # a `WHERE a >= 50 ORDER BY b ASC LIMIT 5` query returns the correct top-5 by | ||
| # `b` while the dynamic predicate prunes a row group during the application of | ||
| # multiple predicates. Layout (RG size 100): | ||
| # RG 0: b=1000..1099, a=100..199 (a>=50 keeps all) | ||
| # RG 1: b=2000..2099, a=0..99 (a>=50 keeps rows 50..99 — page-index prunes | ||
| # the first 5 pages, leaving `skip 50, select 50`) | ||
| # RG 2: b=3000..3099, a=100..199 (keeps all) | ||
| # RG 3: b=0..99, a=100..199 (keeps all) | ||
| # The correct top-5 by `b` (0..4) lives entirely in RG 3. | ||
| # `data_page_row_count_limit`/`write_batch_size` force multiple pages per RG so | ||
| # page-index pruning can produce the intra-RG selection. | ||
| # Tracking issue for the behavior change (keeping both mechanisms): | ||
| # https://github.com/apache/arrow-rs/issues/10624 / | ||
| # https://github.com/apache/datafusion/issues/24358 | ||
| statement ok | ||
| set datafusion.execution.target_partitions = 1; | ||
|
|
||
| statement ok | ||
| set datafusion.execution.parquet.pushdown_filters = true; | ||
|
|
||
| statement ok | ||
| CREATE TABLE rgsel_src AS | ||
| SELECT | ||
| CAST(CASE WHEN i / 100 = 1 THEN i % 100 ELSE 100 + (i % 100) END AS BIGINT) AS a, | ||
| CAST(CASE | ||
| WHEN i < 100 THEN 1000 + i | ||
| WHEN i < 200 THEN 2000 + (i - 100) | ||
| WHEN i < 300 THEN 3000 + (i - 200) | ||
| ELSE (i - 300) | ||
| END AS BIGINT) AS b | ||
| FROM generate_series(0, 399) AS t(i); | ||
|
|
||
| statement ok | ||
| COPY (SELECT * FROM rgsel_src) | ||
| TO 'test_files/scratch/dynamic_row_group_pruning/rgsel.parquet' | ||
| STORED AS PARQUET | ||
| OPTIONS ( | ||
| 'format.max_row_group_size' '100', | ||
| 'format.data_page_row_count_limit' '10', | ||
| 'format.write_batch_size' '10' | ||
| ); | ||
|
|
||
| statement ok | ||
| drop table rgsel_src; | ||
|
|
||
| statement ok | ||
| CREATE EXTERNAL TABLE rgsel (a BIGINT NOT NULL, b BIGINT NOT NULL) | ||
| STORED AS PARQUET | ||
| LOCATION 'test_files/scratch/dynamic_row_group_pruning/rgsel.parquet'; | ||
|
|
||
| # The correct top-5 by `b` among rows with `a >= 50` is b = 0..4 (they live in | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you also please update the test so it runs the same query without filter pushdown so it is clear the answers are the same? |
||
| # RG 3, all of whose rows satisfy `a >= 50`). | ||
| query I | ||
| SELECT b FROM rgsel WHERE a >= 50 ORDER BY b ASC LIMIT 5; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I reverted the code change in this PR and ran and it fails like this . query result mismatch:
[SQL] SELECT b FROM rgsel WHERE a >= 50 ORDER BY b ASC LIMIT 5;
[Diff] (-expected|+actual)
- 0
- 1
- 2
- 3
- 4
+ 50
+ 51
+ 52
+ 53
+ 54
at /private/tmp/df-24359-ablation/datafusion/sqllogictest/test_files/dynamic_row_group_pruning.slt:252(as expected) In case anyone else is interested in what is in the file Details> select * from './datafusion/sqllogictest/test_files/scratch/dynamic_row_group_pruning/rgsel.parquet';
+-----+------+
| a | b |
+-----+------+
| 100 | 1000 |
| 101 | 1001 |
| 102 | 1002 |
| 103 | 1003 |
| 104 | 1004 |
| 105 | 1005 |
| 106 | 1006 |
| 107 | 1007 |
| 108 | 1008 |
| 109 | 1009 |
| 110 | 1010 |
| 111 | 1011 |
| 112 | 1012 |
| 113 | 1013 |
| 114 | 1014 |
| 115 | 1015 |
| 116 | 1016 |
| 117 | 1017 |
| 118 | 1018 |
| 119 | 1019 |
| 120 | 1020 |
| 121 | 1021 |
| 122 | 1022 |
| 123 | 1023 |
| 124 | 1024 |
| 125 | 1025 |
| 126 | 1026 |
| 127 | 1027 |
| 128 | 1028 |
| 129 | 1029 |
| 130 | 1030 |
| 131 | 1031 |
| 132 | 1032 |
| 133 | 1033 |
| 134 | 1034 |
| 135 | 1035 |
| 136 | 1036 |
| 137 | 1037 |
| 138 | 1038 |
| 139 | 1039 |
| . |
| . |
| . |
+-----+------+And the whole results > SELECT a, b FROM './datafusion/sqllogictest/test_files/scratch/dynamic_row_group_pruning/rgsel.parquet' WHERE a >= 50 ORDER BY b ASC LIMIT 5;
+-----+---+
| a | b |
+-----+---+
| 100 | 0 |
| 101 | 1 |
| 102 | 2 |
| 103 | 3 |
| 104 | 4 |
+-----+---+ |
||
| ---- | ||
| 0 | ||
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | ||
|
|
||
| # The same query without filter pushdown never engages the runtime pruner, so | ||
| # its answer is the ground truth the pushdown path above must match. | ||
| statement ok | ||
| set datafusion.execution.parquet.pushdown_filters = false; | ||
|
|
||
| query I | ||
| SELECT b FROM rgsel WHERE a >= 50 ORDER BY b ASC LIMIT 5; | ||
| ---- | ||
| 0 | ||
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | ||
|
|
||
| statement ok | ||
| set datafusion.execution.parquet.pushdown_filters = true; | ||
|
|
||
| statement ok | ||
| drop table rgsel; | ||
|
|
||
| # The SLT runner sets `target_partitions` to 4 instead of using the default, so | ||
| # restore it explicitly rather than RESET (which would revert to the system | ||
| # default = num_cpus and leak modified config out of this file). | ||
| statement ok | ||
| set datafusion.execution.target_partitions = 4; | ||
|
|
||
| statement ok | ||
| RESET datafusion.execution.parquet.pushdown_filters; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe here would be a better place to add the tracking ticket for the change in behavior so it is clear what is expected to change when this feature is implemented