-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Support page skipping / page_index pushdown for evolved schemas #5268
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
Changes from all commits
038b643
0ae899e
e3abcb5
d154b9e
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 |
---|---|---|
|
@@ -29,7 +29,7 @@ | |
//! other source (e.g. a catalog) | ||
|
||
use std::convert::TryFrom; | ||
use std::{collections::HashSet, sync::Arc}; | ||
use std::sync::Arc; | ||
|
||
use crate::execution::context::ExecutionProps; | ||
use crate::prelude::lit; | ||
|
@@ -233,25 +233,18 @@ impl PruningPredicate { | |
.unwrap_or_default() | ||
} | ||
|
||
/// Returns all need column indexes to evaluate this pruning predicate | ||
pub(crate) fn need_input_columns_ids(&self) -> HashSet<usize> { | ||
let mut set = HashSet::new(); | ||
self.required_columns.columns.iter().for_each(|x| { | ||
match self.schema().column_with_name(x.0.name.as_str()) { | ||
None => {} | ||
Some(y) => { | ||
set.insert(y.0); | ||
} | ||
} | ||
}); | ||
set | ||
pub(crate) fn required_columns(&self) -> &RequiredStatColumns { | ||
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. This is the core change -- 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. Thanks for explanation! 👍
I have a question about if 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. And file_a (c1, c2), file_b(c3) , support create external table t(c1)? 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 see both situation support in below test 😆 |
||
&self.required_columns | ||
} | ||
} | ||
|
||
/// Records for which columns statistics are necessary to evaluate a | ||
/// pruning predicate. | ||
/// | ||
/// Handles creating references to the min/max statistics | ||
/// for columns as well as recording which statistics are needed | ||
#[derive(Debug, Default, Clone)] | ||
struct RequiredStatColumns { | ||
pub(crate) struct RequiredStatColumns { | ||
/// The statistics required to evaluate this predicate: | ||
/// * The unqualified column in the input schema | ||
/// * Statistics type (e.g. Min or Max or Null_Count) | ||
|
@@ -267,7 +260,7 @@ impl RequiredStatColumns { | |
|
||
/// Returns an iterator over items in columns (see doc on | ||
/// `self.columns` for details) | ||
fn iter(&self) -> impl Iterator<Item = &(Column, StatisticsType, Field)> { | ||
pub(crate) fn iter(&self) -> impl Iterator<Item = &(Column, StatisticsType, Field)> { | ||
self.columns.iter() | ||
} | ||
|
||
|
@@ -852,7 +845,7 @@ fn build_statistics_expr(expr_builder: &mut PruningExpressionBuilder) -> Result< | |
} | ||
|
||
#[derive(Debug, Copy, Clone, PartialEq, Eq)] | ||
enum StatisticsType { | ||
pub(crate) enum StatisticsType { | ||
Min, | ||
Max, | ||
NullCount, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -836,8 +836,7 @@ mod tests { | |
|
||
/// round-trip record batches by writing each individual RecordBatch to | ||
/// a parquet file and then reading that parquet file with the specified | ||
/// options. If page_index_predicate is set to `true`, all RecordBatches | ||
/// are written into a parquet file instead. | ||
/// options. | ||
#[derive(Debug, Default)] | ||
struct RoundTrip { | ||
projection: Option<Vec<usize>>, | ||
|
@@ -1331,6 +1330,80 @@ mod tests { | |
assert_eq!(get_value(&metrics, "pushdown_rows_filtered"), 5); | ||
} | ||
|
||
#[tokio::test] | ||
async fn evolved_schema_disjoint_schema_with_page_index_pushdown() { | ||
let c1: ArrayRef = Arc::new(StringArray::from(vec![ | ||
// Page 1 | ||
Some("Foo"), | ||
Some("Bar"), | ||
// Page 2 | ||
Some("Foo2"), | ||
Some("Bar2"), | ||
// Page 3 | ||
Some("Foo3"), | ||
Some("Bar3"), | ||
])); | ||
|
||
let c2: ArrayRef = Arc::new(Int64Array::from(vec![ | ||
// Page 1: | ||
Some(1), | ||
Some(2), | ||
// Page 2: (pruned) | ||
Some(3), | ||
Some(4), | ||
// Page 3: (pruned) | ||
Some(5), | ||
None, | ||
])); | ||
|
||
// batch1: c1(string) | ||
let batch1 = create_batch(vec![("c1", c1.clone())]); | ||
|
||
// batch2: c2(int64) | ||
let batch2 = create_batch(vec![("c2", c2.clone())]); | ||
|
||
// batch3 (has c2, c1) -- both columns, should still prune | ||
let batch3 = create_batch(vec![("c1", c1.clone()), ("c2", c2.clone())]); | ||
|
||
// batch4 (has c2, c1) -- different column order, should still prune | ||
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. Nice test case 👍 |
||
let batch4 = create_batch(vec![("c2", c2), ("c1", c1)]); | ||
|
||
let filter = col("c2").eq(lit(1_i64)); | ||
|
||
// read/write them files: | ||
let rt = RoundTrip::new() | ||
.with_predicate(filter) | ||
.with_page_index_predicate() | ||
.round_trip(vec![batch1, batch2, batch3, batch4]) | ||
.await; | ||
|
||
let expected = vec![ | ||
"+------+----+", | ||
"| c1 | c2 |", | ||
"+------+----+", | ||
"| | 1 |", | ||
"| | 2 |", | ||
"| Bar | |", | ||
"| Bar | 2 |", | ||
"| Bar | 2 |", | ||
"| Bar2 | |", | ||
"| Bar3 | |", | ||
"| Foo | |", | ||
"| Foo | 1 |", | ||
"| Foo | 1 |", | ||
"| Foo2 | |", | ||
"| Foo3 | |", | ||
"+------+----+", | ||
]; | ||
assert_batches_sorted_eq!(expected, &rt.batches.unwrap()); | ||
let metrics = rt.parquet_exec.metrics().unwrap(); | ||
|
||
// There are 4 rows pruned in each of batch2, batch3, and | ||
// batch4 for a total of 12. batch1 had no pruning as c2 was | ||
// filled in as null | ||
assert_eq!(get_value(&metrics, "page_index_rows_filtered"), 12); | ||
} | ||
|
||
#[tokio::test] | ||
async fn multi_column_predicate_pushdown() { | ||
let c1: ArrayRef = | ||
|
@@ -1362,6 +1435,38 @@ mod tests { | |
assert_batches_sorted_eq!(expected, &read); | ||
} | ||
|
||
#[tokio::test] | ||
async fn multi_column_predicate_pushdown_page_index_pushdown() { | ||
let c1: ArrayRef = | ||
Arc::new(StringArray::from(vec![Some("Foo"), None, Some("bar")])); | ||
|
||
let c2: ArrayRef = Arc::new(Int64Array::from(vec![Some(1), Some(2), None])); | ||
|
||
let batch1 = create_batch(vec![("c1", c1.clone()), ("c2", c2.clone())]); | ||
|
||
// Columns in different order to schema | ||
let filter = col("c2").eq(lit(1_i64)).or(col("c1").eq(lit("bar"))); | ||
|
||
// read/write them files: | ||
let read = RoundTrip::new() | ||
.with_predicate(filter) | ||
.with_page_index_predicate() | ||
.round_trip_to_batches(vec![batch1]) | ||
.await | ||
.unwrap(); | ||
|
||
let expected = vec![ | ||
"+-----+----+", | ||
"| c1 | c2 |", | ||
"+-----+----+", | ||
"| | 2 |", | ||
"| Foo | 1 |", | ||
"| bar | |", | ||
"+-----+----+", | ||
]; | ||
assert_batches_sorted_eq!(expected, &read); | ||
} | ||
|
||
#[tokio::test] | ||
async fn evolved_schema_incompatible_types() { | ||
let c1: ArrayRef = | ||
|
@@ -1635,27 +1740,38 @@ mod tests { | |
|
||
#[tokio::test] | ||
async fn parquet_page_index_exec_metrics() { | ||
let c1: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), None, Some(2)])); | ||
let c2: ArrayRef = Arc::new(Int32Array::from(vec![Some(3), Some(4), Some(5)])); | ||
let c1: ArrayRef = Arc::new(Int32Array::from(vec![ | ||
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. This was the only test that used the "merge multiple batches together" behavior of |
||
Some(1), | ||
None, | ||
Some(2), | ||
Some(3), | ||
Some(4), | ||
Some(5), | ||
])); | ||
let batch1 = create_batch(vec![("int", c1.clone())]); | ||
let batch2 = create_batch(vec![("int", c2.clone())]); | ||
|
||
let filter = col("int").eq(lit(4_i32)); | ||
|
||
let rt = RoundTrip::new() | ||
.with_predicate(filter) | ||
.with_page_index_predicate() | ||
.round_trip(vec![batch1, batch2]) | ||
.round_trip(vec![batch1]) | ||
.await; | ||
|
||
let metrics = rt.parquet_exec.metrics().unwrap(); | ||
|
||
// assert the batches and some metrics | ||
#[rustfmt::skip] | ||
let expected = vec![ | ||
"+-----+", "| int |", "+-----+", "| 3 |", "| 4 |", "| 5 |", "+-----+", | ||
"+-----+", | ||
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. this is different because previously the page layout was as follows Page1: Page 2 Now the page layout is Page1: Page2 Page3 |
||
"| int |", | ||
"+-----+", | ||
"| 4 |", | ||
"| 5 |", | ||
"+-----+", | ||
]; | ||
assert_batches_sorted_eq!(expected, &rt.batches.unwrap()); | ||
assert_eq!(get_value(&metrics, "page_index_rows_filtered"), 3); | ||
assert_eq!(get_value(&metrics, "page_index_rows_filtered"), 4); | ||
assert!( | ||
get_value(&metrics, "page_index_eval_time") > 0, | ||
"no eval time in metrics: {metrics:#?}" | ||
|
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.
This change is so I can actually test evolved schemas with page indexes (aka write multiple files with different schemas)