-
Notifications
You must be signed in to change notification settings - Fork 151
Mimic duckdb's post-filter cardinality estimates #7895
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
base: develop
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| //! to get a blanket [`TableFunction`] implementation covering init, scan, progress, filter | ||
| //! pushdown, cardinality, and partitioning. | ||
|
|
||
| use std::cmp::max; | ||
| use std::fmt::Debug; | ||
| use std::ops::Range; | ||
| use std::sync::Arc; | ||
|
|
@@ -51,6 +52,8 @@ use vortex::layout::scan::multi::MultiLayoutDataSource; | |
| use vortex::metrics::tracing::get_global_labels; | ||
| use vortex::scalar::Scalar; | ||
| use vortex::scalar::ScalarValue; | ||
| use vortex::scalar_fn::fns::binary::Binary; | ||
| use vortex::scalar_fn::fns::operators::Operator; | ||
| use vortex::scalar_fn::fns::pack::Pack; | ||
| use vortex::scan::DataSource; | ||
| use vortex::scan::ScanRequest; | ||
|
|
@@ -74,6 +77,7 @@ use crate::duckdb::DuckdbStringMapRef; | |
| use crate::duckdb::ExpressionRef; | ||
| use crate::duckdb::LogicalType; | ||
| use crate::duckdb::PartitionData; | ||
| use crate::duckdb::TableFilterClass; | ||
| use crate::duckdb::TableFilterSetRef; | ||
| use crate::duckdb::TableFunction; | ||
| use crate::duckdb::TableInitInput; | ||
|
|
@@ -118,6 +122,7 @@ pub struct DataSourceBindData { | |
| data_source: Arc<MultiLayoutDataSource>, | ||
| filter_exprs: Vec<Expression>, | ||
| column_fields: Vec<DuckdbField>, | ||
| has_non_optional_filter: bool, | ||
| } | ||
|
|
||
| impl Clone for DataSourceBindData { | ||
|
|
@@ -127,6 +132,7 @@ impl Clone for DataSourceBindData { | |
| // filter_exprs are consumed once in `init_global`. | ||
| filter_exprs: vec![], | ||
| column_fields: self.column_fields.clone(), | ||
| has_non_optional_filter: self.has_non_optional_filter, | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -252,6 +258,20 @@ impl ColumnStatisticsAggregate { | |
| } | ||
| } | ||
|
|
||
| // Duckdb requires post-filter cardinality estimates, otherwise join | ||
| // planner may flip join sides which is a huge regression for some | ||
| // queries i.e. 1000x for tpcds 85. | ||
| // | ||
| // See duckdb/src/optimizer/join_order/relation_statistics_helper.cpp | ||
| const DEFAULT_SELECTIVITY: f64 = 0.2; | ||
| fn postfilter_cardinality(cardinality: u64, has_non_optional_filter: bool) -> u64 { | ||
| if has_non_optional_filter { | ||
| max(1, (cardinality as f64 * DEFAULT_SELECTIVITY) as u64) | ||
| } else { | ||
| cardinality | ||
| } | ||
| } | ||
|
|
||
| impl<T: DataSourceTableFunction> TableFunction for T { | ||
| type BindData = DataSourceBindData; | ||
| type GlobalState = DataSourceGlobal; | ||
|
|
@@ -275,6 +295,7 @@ impl<T: DataSourceTableFunction> TableFunction for T { | |
| data_source: Arc::new(data_source), | ||
| filter_exprs: vec![], | ||
| column_fields, | ||
| has_non_optional_filter: false, | ||
| }) | ||
| } | ||
|
|
||
|
|
@@ -297,13 +318,15 @@ impl<T: DataSourceTableFunction> TableFunction for T { | |
| row_range, | ||
| file_selection, | ||
| file_range, | ||
| has_non_optional_filter, | ||
| } = extract_table_filter_expr( | ||
| init_input.table_filter_set(), | ||
| column_ids, | ||
| &bind_data.column_fields, | ||
| &bind_data.filter_exprs, | ||
| bind_data.data_source.dtype(), | ||
| )?; | ||
| bind_data.has_non_optional_filter = has_non_optional_filter; | ||
|
|
||
| debug!( | ||
| %projection, | ||
|
|
@@ -506,18 +529,19 @@ impl<T: DataSourceTableFunction> TableFunction for T { | |
| debug!(%expr, "failed to push down expression"); | ||
| return Ok(false); | ||
| }; | ||
| debug!(%expr, "pushed down expression"); | ||
| bind_data.filter_exprs.push(expr); | ||
|
|
||
| // NOTE(ngates): Vortex does indeed run exact filters, so in theory we should return `true` | ||
| // here to tell DuckDB we've handled the filter. However, DuckDB applies some crude | ||
| // cardinality estimation heuristics (e.g. an equality filter => 20% selectivity) that | ||
| // means by returning false, DuckDB runs an additional filter (a little bit of overhead) | ||
| // but tends to end up with a better query plan. | ||
| // If we plumb row count estimation into the layout tree, perhaps we could use zone maps | ||
| // etc. to return estimates. But this function is probably called too late anyway. Maybe | ||
| // we need our own cardinality heuristics. | ||
| Ok(false) | ||
| // Default selectivity filter gives too high cardinality bounds for | ||
| // equality operators which flips join sides, see tpch sf=10, query 17. | ||
| // All other operators estimate is mostly correct. | ||
| // To fix this, we need to start collecting distinct counts for columns. | ||
| let report_pushed = !expr | ||
| .as_opt::<Binary>() | ||
| .map(|op| *op == Operator::Eq) | ||
| .unwrap_or(false); | ||
|
Comment on lines
+537
to
+540
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. what about ( |
||
|
|
||
| debug!(%expr, report_pushed, "pushed down expression"); | ||
| bind_data.filter_exprs.push(expr); | ||
| Ok(report_pushed) | ||
| } | ||
|
|
||
| /// Get column-wise statistics. Available only if we're reading a single | ||
|
|
@@ -545,8 +569,10 @@ impl<T: DataSourceTableFunction> TableFunction for T { | |
|
|
||
| fn cardinality(bind_data: &Self::BindData) -> Cardinality { | ||
| match bind_data.data_source.row_count() { | ||
| Some(Precision::Exact(v)) => Cardinality::Maximum(v), | ||
| Some(Precision::Inexact(v)) => Cardinality::Estimate(v), | ||
| Some(Precision::Exact(v) | Precision::Inexact(v)) => { | ||
| // Post-filter estimate is always a heuristic. | ||
| Cardinality::Estimate(postfilter_cardinality(v, bind_data.has_non_optional_filter)) | ||
| } | ||
| None => Cardinality::Unknown, | ||
| } | ||
| } | ||
|
|
@@ -565,8 +591,8 @@ impl<T: DataSourceTableFunction> TableFunction for T { | |
| fn to_string(bind_data: &Self::BindData, map: &mut DuckdbStringMapRef) { | ||
| map.push("Function", "Vortex Scan"); | ||
| if !bind_data.filter_exprs.is_empty() { | ||
| let mut filters = bind_data.filter_exprs.iter().map(|f| format!("{}", f)); | ||
| map.push("Filters", &filters.join(" /\\\n")); | ||
| let mut filters = bind_data.filter_exprs.iter().map(|f| format!("{f}")); | ||
| map.push("Filters", &filters.join("\n")); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -687,6 +713,7 @@ struct FilterWithVirtualColumns { | |
| row_range: Option<Range<u64>>, | ||
| file_selection: Selection, | ||
| file_range: Option<Range<u64>>, | ||
| has_non_optional_filter: bool, | ||
|
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. define this!
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. What does duckdb do? what about
Contributor
Author
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 what duckdb does. The only distinction is "at least one non-optional filter" |
||
| } | ||
|
|
||
| /// Creates a table filter expression, row selection, and row range from the table filter set, | ||
|
|
@@ -698,6 +725,8 @@ fn extract_table_filter_expr( | |
| additional_filters: &[Expression], | ||
| dtype: &DType, | ||
| ) -> VortexResult<FilterWithVirtualColumns> { | ||
| let mut has_non_optional_filter = false; | ||
|
|
||
| let mut table_filter_exprs: HashSet<Expression> = if let Some(filter) = table_filter_set { | ||
| filter | ||
| .into_iter() | ||
|
|
@@ -706,6 +735,8 @@ fn extract_table_filter_expr( | |
| !is_virtual_column(column_ids[idx_u]) | ||
| }) | ||
| .map(|(idx, ex)| { | ||
| has_non_optional_filter |= !matches!(ex.as_class(), TableFilterClass::Optional(_)); | ||
|
|
||
| let idx_u: usize = idx.as_(); | ||
| let col_idx: usize = column_ids[idx_u].as_(); | ||
| let name = &column_fields.get(col_idx).vortex_expect("exists").name; | ||
|
|
@@ -741,6 +772,7 @@ fn extract_table_filter_expr( | |
| row_range, | ||
| file_selection, | ||
| file_range, | ||
| has_non_optional_filter, | ||
| }; | ||
| Ok(out) | ||
| } | ||
|
|
||
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.
should this be in the c++ wrapper,
I would like this to be in rust