Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
397 changes: 397 additions & 0 deletions datafusion/core/tests/physical_optimizer/filter_pushdown.rs

Large diffs are not rendered by default.

35 changes: 25 additions & 10 deletions datafusion/physical-plan/src/aggregates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ use crate::{
};
use datafusion_common::config::ConfigOptions;
use parking_lot::Mutex;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;

use arrow::array::{ArrayRef, UInt8Array, UInt16Array, UInt32Array, UInt64Array};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
Expand Down Expand Up @@ -2268,13 +2268,27 @@ impl ExecutionPlan for AggregateExec {
// the result of SUM or COUNT), as those require computing all groups first.

// Grouping columns are output before aggregate columns, in the same order
// as the grouping expressions. A grouping-set null mask marks grouping
// columns that are not available in that set.
let mut allowed_indices: HashSet<usize> =
(0..self.group_by.expr().len()).collect();
for null_mask in self.group_by.groups() {
allowed_indices.retain(|idx| null_mask.get(*idx) != Some(&true));
}
// as the grouping expressions. Map each grouping output position to the
// input column it reads, by position rather than by name, so that
// same-named grouping columns stay distinct. Only grouping expressions
// that are plain input columns can be mapped; a grouping-set null mask
// marks grouping columns that are not available in that set.
let column_mapping: HashMap<usize, usize> = self
.group_by
.expr()
.iter()
.enumerate()
.filter(|(idx, _)| {
self.group_by
.groups()
.iter()
.all(|null_mask| null_mask.get(*idx) != Some(&true))
})
.filter_map(|(idx, (expr, _))| {
expr.downcast_ref::<Column>()
.map(|column| (idx, column.index()))
})
.collect();

let child = self.children()[0];
// Global aggregates and grouping sets containing an empty grouping set
Expand All @@ -2290,9 +2304,9 @@ impl ExecutionPlan for AggregateExec {
let mut child_desc = if may_emit_on_empty_input {
ChildFilterDescription::all_unsupported(&parent_filters)
} else {
ChildFilterDescription::from_child_with_allowed_indices(
ChildFilterDescription::from_child_with_column_mapping(
&parent_filters,
allowed_indices,
column_mapping,
child,
)?
};
Expand Down Expand Up @@ -3238,6 +3252,7 @@ pub fn evaluate_group_by(

#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::task::{Context, Poll};

use super::*;
Expand Down
76 changes: 55 additions & 21 deletions datafusion/physical-plan/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use crate::common::can_project;
use crate::execution_plan::{CardinalityEffect, replace_children_if_necessary};
use crate::filter_pushdown::{
ChildFilterDescription, ChildPushdownResult, FilterDescription, FilterPushdownPhase,
FilterPushdownPropagation, PushedDown,
FilterPushdownPropagation, FilterRemapper, PushedDown,
};
use crate::limit::LocalLimitExec;
use crate::metrics::{MetricBuilder, MetricType};
Expand All @@ -57,7 +57,8 @@ use datafusion_common::config::ConfigOptions;
use datafusion_common::stats::Precision;
use datafusion_common::tree_node::TreeNodeRecursion;
use datafusion_common::{
DataFusionError, Result, ScalarValue, internal_err, plan_err, project_schema,
DataFusionError, Result, ScalarValue, internal_datafusion_err, internal_err,
plan_err, project_schema,
};
use datafusion_execution::TaskContext;
use datafusion_expr::Operator;
Expand All @@ -66,7 +67,7 @@ use datafusion_physical_expr::expressions::{
BinaryExpr, Column, InListExpr, IsNotNullExpr, Literal, lit,
};
use datafusion_physical_expr::intervals::utils::check_support;
use datafusion_physical_expr::utils::{collect_columns, reassign_expr_columns};
use datafusion_physical_expr::utils::collect_columns;
use datafusion_physical_expr::{
AcrossPartitions, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr, analyze,
conjunction, split_conjunction,
Expand Down Expand Up @@ -316,6 +317,29 @@ impl FilterExec {
self.default_selectivity
}

/// Describe which parent filters (in this node's output coordinates) can
/// be forwarded to the input, remapped into input coordinates.
///
/// With an embedded projection the output position `i` reads input column
/// `projection[i]`; without one the positions are identical. Mapping by
/// position keeps same-named input columns distinct.
fn parent_filters_for_input(
&self,
parent_filters: &[Arc<dyn PhysicalExpr>],
) -> Result<ChildFilterDescription> {
if parent_filters.is_empty() {
return Ok(ChildFilterDescription::empty());
}
match self.projection.as_ref() {
Some(projection) => ChildFilterDescription::from_child_with_column_mapping(
parent_filters,
projection.iter().copied().enumerate().collect(),
self.input(),
),
None => ChildFilterDescription::from_child(parent_filters, self.input()),
}
}

/// Projection
pub fn projection(&self) -> &Option<ProjectionRef> {
&self.projection
Expand Down Expand Up @@ -697,21 +721,18 @@ impl ExecutionPlan for FilterExec {
parent_filters: Vec<Arc<dyn PhysicalExpr>>,
_config: &ConfigOptions,
) -> Result<FilterDescription> {
if phase != FilterPushdownPhase::Pre {
let child =
ChildFilterDescription::from_child(&parent_filters, self.input())?;
return Ok(FilterDescription::new().with_child(child));
let mut child = self.parent_filters_for_input(&parent_filters);
if phase == FilterPushdownPhase::Pre {
child = child.map(|child| {
child.with_self_filters(
split_conjunction(&self.predicate)
.into_iter()
.cloned()
.collect(),
)
});
}

let child = ChildFilterDescription::from_child(&parent_filters, self.input())?
.with_self_filters(
split_conjunction(&self.predicate)
.into_iter()
.cloned()
.collect(),
);

Ok(FilterDescription::new().with_child(child))
child.map(|child| FilterDescription::new().with_child(child))
}

fn handle_child_pushdown_result(
Expand All @@ -735,12 +756,25 @@ impl ExecutionPlan for FilterExec {

// If this FilterExec has a projection, the unsupported parent filters
// are in the output schema (after projection) coordinates. We need to
// remap them to the input schema coordinates before combining with self filters.
if self.projection.is_some() {
let input_schema = self.input().schema();
// remap them to the input schema coordinates before combining with self
// filters. Map by position through the projection: the input may
// contain several columns with the same name.
if let Some(projection) = self.projection.as_ref()
&& !unsupported_parent_filters.is_empty()
{
let remapper = FilterRemapper::with_column_mapping(
self.input().schema(),
projection.iter().copied().enumerate().collect(),
);
unsupported_parent_filters = unsupported_parent_filters
.into_iter()
.map(|expr| reassign_expr_columns(expr, &input_schema))
.map(|expr| {
remapper.try_remap(&expr)?.ok_or_else(|| {
internal_datafusion_err!(
"Parent filter {expr} references a column that is not in the FilterExec projection {projection:?}"
)
})
})
.collect::<Result<Vec<_>>>()?;
}

Expand Down
Loading