You are viewing a plain text version of this content. The canonical link for it is here.
Posted to github@datafusion.apache.org by "andygrove (via GitHub)" <gi...@apache.org> on 2024/08/16 12:21:46 UTC
[PR] perf: Remove redundant copies of batches after FilterExec [datafusion-comet]
andygrove opened a new pull request, #835:
URL: https://github.com/apache/datafusion-comet/pull/835
## Which issue does this PR close?
<!--
We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. For example `Closes #123` indicates that this PR will close issue #123.
-->
Closes https://github.com/apache/datafusion-comet/issues/757
## Rationale for this change
<!--
Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes.
-->
We were often creating a filtered batch in FilterExec and then making a copy of that batch in CopyExec, resulting in redundant copying of data.

This reduces our TPC-DS time by ~25 seconds.
The majority of queries are faster with this change:

## What changes are included in this PR?
<!--
There is no need to duplicate the description in the issue here but it is sometimes worth providing a summary of the individual changes in this PR.
-->
- Copy DataFusion's FilterExec into Comet and make a one line change so that FilterExec never passes through input batches -- I will suggest some changes upstream so that we can customize rather than duplicate
- Stop wrapping FilterExec in CopyExec
-
## How are these changes tested?
<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code
If tests are not included in your PR, please explain why (for example, are they covered by existing tests)?
-->
- Existing tests
- I ran TPC-DS without error
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720379684
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
`clone` will have a shallow copy with the buffers are still shared, but we need actually copying the data.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720387068
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
The buffer implementation under the arrays holds references. `clone` will just copy the references without copying data.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720384649
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
I thought `copy()` would make a shallow copy, while `clone()` would deep copy the data?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copies of batches after FilterExec [datafusion-comet]
Posted by "codecov-commenter (via GitHub)" <gi...@apache.org>.
codecov-commenter commented on PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#issuecomment-2293652009
## [Codecov](https://app.codecov.io/gh/apache/datafusion-comet/pull/835?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 33.95%. Comparing base [(`380f03d`)](https://app.codecov.io/gh/apache/datafusion-comet/commit/380f03d067b7703adefd3dce1849d9709bad3da9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) to head [(`9458cfe`)](https://app.codecov.io/gh/apache/datafusion-comet/commit/9458cfe09b309df5d5a2407e20552c43a512781b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
> Report is 1 commits behind head on main.
<details><summary>Additional details and impacted files</summary>
```diff
@@ Coverage Diff @@
## main #835 +/- ##
============================================
+ Coverage 33.83% 33.95% +0.12%
- Complexity 870 880 +10
============================================
Files 112 112
Lines 42970 42993 +23
Branches 9466 9473 +7
============================================
+ Hits 14538 14598 +60
+ Misses 25446 25404 -42
- Partials 2986 2991 +5
```
</details>
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/apache/datafusion-comet/pull/835?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "Dandandan (via GitHub)" <gi...@apache.org>.
Dandandan commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720232849
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
+ for i in 0..predicate.len() {
+ if !predicate.is_null(i) && predicate.value(i) {
+ sv.append_value(i as i32);
+ }
+ }
+ let sv = sv.finish();
+ // note that this does not unpack dictionary-encoded arrays
+ take_record_batch(record_batch, &sv)
Review Comment:
Ah, this is done in`CopyExec` already - this should be faster.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720384649
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
I thought `copy()` would make a shallow copy, while `clone()` would copy the data?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720168929
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
+ for i in 0..predicate.len() {
+ if !predicate.is_null(i) && predicate.value(i) {
+ sv.append_value(i as i32);
+ }
+ }
+ let sv = sv.finish();
+ // note that this does not unpack dictionary-encoded arrays
+ take_record_batch(record_batch, &sv)
Review Comment:
I think `take_record_batch` actually copies values based on indices. So even the batch is not filtered (all selected), it will copy the batch. Is it different to `CopyExec`?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "Dandandan (via GitHub)" <gi...@apache.org>.
Dandandan commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720161761
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
+ for i in 0..predicate.len() {
+ if !predicate.is_null(i) && predicate.value(i) {
+ sv.append_value(i as i32);
+ }
+ }
+ let sv = sv.finish();
+ // note that this does not unpack dictionary-encoded arrays
+ take_record_batch(record_batch, &sv)
Review Comment:
Probably a faster way to copy is to use MutableArrayData, like done in concatenate:
https://docs.rs/arrow-select/52.2.0/src/arrow_select/concat.rs.html#180
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720281899
##########
native/Cargo.toml:
##########
@@ -44,6 +44,7 @@ datafusion = { default-features = false, git = "https://github.com/apache/datafu
datafusion-functions = { git = "https://github.com/apache/datafusion.git", rev = "41.0.0-rc1", features = ["crypto_expressions"] }
datafusion-functions-nested = { git = "https://github.com/apache/datafusion.git", rev = "41.0.0-rc1", default-features = false }
datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "41.0.0-rc1", default-features = false }
+datafusion-execution = { git = "https://github.com/apache/datafusion.git", rev = "41.0.0-rc1", default-features = false }
Review Comment:
We need `datafusion-execution`?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#issuecomment-2294410608
Thanks for the reviews @viirya @Dandandan @mbutrovich
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720392618
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
So `RecordBatch` stores a `Vec` of reference counted arrays `columns: Vec<Arc<dyn Array>>`? That makes sense to me that if we `clone()` we just bump ref counts. But the underlying `ArrayData` should be safe to `clone()` because its underlying `Vec` will deep copy its buffer.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "Dandandan (via GitHub)" <gi...@apache.org>.
Dandandan commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720154103
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
Review Comment:
Probably special casing the edge case on `true_count` == `num_rows` and defaulting to `filter` otherwise would avoid regressing "normal filtering"
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720384649
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
I thought `copy()` would make a shallow copy, while `clone()` would deep copy the data? If that were the case, we could possibly `clone()` the whole `RecordBatch`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#issuecomment-2294208375
The results are even better now :tada:


--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720277302
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
+ for i in 0..predicate.len() {
+ if !predicate.is_null(i) && predicate.value(i) {
+ sv.append_value(i as i32);
+ }
+ }
+ let sv = sv.finish();
+ // note that this does not unpack dictionary-encoded arrays
+ take_record_batch(record_batch, &sv)
Review Comment:
That is correct. The goal is to ensure that FilterExec *always* copies instead of only copying most of the time, so that we avoid also having a CopyExec that performs yet another copy.
I have implemented the suggestion from @Dandandan to specialize for the case where all rows are selected
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "Dandandan (via GitHub)" <gi...@apache.org>.
Dandandan commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720278873
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
+ for i in 0..predicate.len() {
+ if !predicate.is_null(i) && predicate.value(i) {
+ sv.append_value(i as i32);
+ }
+ }
+ let sv = sv.finish();
+ // note that this does not unpack dictionary-encoded arrays
+ take_record_batch(record_batch, &sv)
Review Comment:
Yes got it. It might be possible though to reuse the same code from copyexec in here for copying the recordbatch?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#issuecomment-2294383241
I created a PR against DataFusion to add a flag to FilterExec so that we can switch back to using the upstream version, assumign this PR gets accepted.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720227963
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
+ for i in 0..predicate.len() {
+ if !predicate.is_null(i) && predicate.value(i) {
Review Comment:
Another possible special case that we should measure the impact of: we could elide `!predicate.is_null(i)` if we know there are no nulls in the predicate array. You end up with duplicated loops (one with and without the null check) but depending on the branch predictor this may be worth it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "Dandandan (via GitHub)" <gi...@apache.org>.
Dandandan commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720154103
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
Review Comment:
Probably special casing the edge case on `predicate.true_count()` == `num_rows` and defaulting to `filter` otherwise would avoid regressing "normal filtering"
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "Dandandan (via GitHub)" <gi...@apache.org>.
Dandandan commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720154103
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
Review Comment:
Probably special casing the edge case on `predicate.true_count()` == `record_batch.num_rows()` and defaulting to `filter` otherwise would avoid regressing "normal filtering"
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#issuecomment-2294105508
After addressing the first round of feedback, we now have:
```rust
pub fn comet_filter_record_batch(
record_batch: &RecordBatch,
predicate: &BooleanArray,
) -> std::result::Result<RecordBatch, ArrowError> {
if predicate.true_count() == record_batch.num_rows() {
// special case where we just make an exact copy
let arrays: Vec<ArrayRef> = record_batch
.columns()
.iter()
.map(|array| {
let capacity = array.len();
let data = array.to_data();
let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
mutable.extend(0, 0, capacity);
make_array(mutable.freeze())
})
.collect();
let options = RecordBatchOptions::new().with_row_count(Some(record_batch.num_rows()));
RecordBatch::try_new_with_options(record_batch.schema().clone(), arrays, &options)
} else {
filter_record_batch(record_batch, predicate)
}
}
```
New benchmark results:
```
filter/comet_filter - few
time: [14.650 µs 14.727 µs 14.831 µs]
change: [-36.702% -35.128% -33.681%] (p = 0.00 < 0.05)
Performance has improved.
Found 2 outliers among 100 measurements (2.00%)
2 (2.00%) high severe
filter/comet_filter - many
time: [75.962 µs 76.172 µs 76.381 µs]
change: [-48.681% -48.501% -48.303%] (p = 0.00 < 0.05)
Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high severe
filter/comet_filter - all
time: [34.497 µs 34.628 µs 34.764 µs]
change: [-80.854% -80.527% -80.256%] (p = 0.00 < 0.05)
Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
6 (6.00%) low mild
1 (1.00%) high mild
1 (1.00%) high severe
```
This certainly looks a lot better. I am running TPC-DS again to make sure this really is always copying. I had tried an approach like this in the past but ran into data corruption issues.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720088234
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
+ for i in 0..predicate.len() {
+ if !predicate.is_null(i) && predicate.value(i) {
+ sv.append_value(i as i32);
+ }
+ }
+ let sv = sv.finish();
+ // note that this does not unpack dictionary-encoded arrays
+ take_record_batch(record_batch, &sv)
+}
+// END Comet changes
Review Comment:
This is the modification to FilterExec
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720372211
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
More of a Rust question for myself to dig into: is there a reason we can't do `array.to_data().clone()` and then call `make_array` on that? It seems inefficient to allocate a resizable data structure, truncate it with `extend()` then `freeze()` it, but I am still very new to this. Thankfully there are good benchmarks in this PR for me to explore that on my own. :)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720392618
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
So `RecordBatch` stores a `Vec` of reference counted arrays `columns: Vec<Arc<dyn Array>>`? That makes sense to me that if we `clone()` `RecordBatch` we just bump ref counts, and we don't get the desired result. But the underlying `ArrayData` should be safe to `clone()` because its underlying `Vec` will deep copy its buffer.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720392618
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
So `RecordBatch` stores a `Vec` of reference counted arrays `columns: Vec<Arc<dyn Array>>`? That makes sense to me that if we `clone()` we just bump ref counts, and we don't get the desired result. But the underlying `ArrayData` should be safe to `clone()` because its underlying `Vec` will deep copy its buffer.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720396638
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
Ah, I see the reference counted stuff all the way down inside `Buffer`. I understand why it won't work now. Thanks!
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720274478
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,450 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn comet_filter_record_batch(
Review Comment:
```suggestion
fn comet_filter_record_batch(
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720397107
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
This is the snippet of `Buffer` implementation in arrow-rs:
```rust
pub struct Buffer {
/// the internal byte buffer.
data: Arc<Bytes>,
...
}
```
And `ArrayData`:
```rust
pub struct ArrayData {
...
buffers: Vec<Buffer>,
...
}
```
When you clone it, `Buffer` won't copy data for you but you only get a clone of `Arc`, i.e., a new reference to the existing data.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720397107
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
This is the snippet of `Buffer` implementation in arrow-rs:
```rust
pub struct Buffer {
/// the internal byte buffer.
data: Arc<Bytes>,
...
}
```
And `ArrayData`:
```rust
pub struct ArrayData {
...
buffers: Vec<Buffer>,
...
}
```
When you clone it, `Buffer` won't copy data for you but you only get a clone of `Arc`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720280941
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,450 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
+ mutable.extend(0, 0, capacity);
+ make_array(mutable.freeze())
+ })
+ .collect();
+ let options = RecordBatchOptions::new().with_row_count(Some(record_batch.num_rows()));
+ RecordBatch::try_new_with_options(record_batch.schema().clone(), arrays, &options)
+ } else {
+ filter_record_batch(record_batch, predicate)
Review Comment:
Yea, it is good to use this arrow-rs kernel instead of call `take` kernel, as `filter_record_batch` has some optimizations for filter selectivity.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720277994
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,450 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn comet_filter_record_batch(
Review Comment:
I think it has to be public so that we can access it from the criterion benchmark but I will double check this.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720286290
##########
native/Cargo.toml:
##########
@@ -44,6 +44,7 @@ datafusion = { default-features = false, git = "https://github.com/apache/datafu
datafusion-functions = { git = "https://github.com/apache/datafusion.git", rev = "41.0.0-rc1", features = ["crypto_expressions"] }
datafusion-functions-nested = { git = "https://github.com/apache/datafusion.git", rev = "41.0.0-rc1", default-features = false }
datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "41.0.0-rc1", default-features = false }
+datafusion-execution = { git = "https://github.com/apache/datafusion.git", rev = "41.0.0-rc1", default-features = false }
Review Comment:
We use `use datafusion::execution::TaskContext`. I guess we were just pulling this in transitively before via the `datafusion` crate rather than being explicit.
We may want to avoid bringing in the core `datafusion` crate and just depend directly on the crates that we need.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#issuecomment-2294022694
Thanks @Dandandan @viirya for the feedback so far. I have now added criterion benchmarks before I start addressing the feedback. Here are the current results.
```
filter/arrow_filter_record_batch - few
time: [15.546 µs 15.601 µs 15.682 µs]
Found 9 outliers among 100 measurements (9.00%)
3 (3.00%) high mild
6 (6.00%) high severe
filter/arrow_filter_record_batch - many
time: [74.935 µs 75.129 µs 75.324 µs]
Found 2 outliers among 100 measurements (2.00%)
2 (2.00%) high mild
filter/arrow_filter_record_batch - all
time: [396.57 ns 397.80 ns 399.05 ns]
filter/comet_filter - few
time: [21.772 µs 22.012 µs 22.337 µs]
Found 14 outliers among 100 measurements (14.00%)
2 (2.00%) high mild
12 (12.00%) high severe
Benchmarking filter/comet_filter - many: Warming up for 500.00 ms
Warning: Unable to complete 100 samples in 500.0ms. You may wish to increase target time to 762.3ms, enable flat sampling, or reduce sample count to 50.
filter/comet_filter - many
time: [146.44 µs 147.25 µs 148.01 µs]
Found 12 outliers among 100 measurements (12.00%)
12 (12.00%) low mild
Benchmarking filter/comet_filter - all: Warming up for 500.00 ms
Warning: Unable to complete 100 samples in 500.0ms. You may wish to increase target time to 895.1ms, enable flat sampling, or reduce sample count to 50.
filter/comet_filter - all
time: [177.25 µs 183.04 µs 190.27 µs]
Found 13 outliers among 100 measurements (13.00%)
2 (2.00%) high mild
11 (11.00%) high severe
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1722007695
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,452 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+// `FilterExec` could modify input batch or return input batch without change. Instead of always
+// adding `CopyExec` on top of it, we only copy input batch for the special case.
+pub fn comet_filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ if predicate.true_count() == record_batch.num_rows() {
+ // special case where we just make an exact copy
+ let arrays: Vec<ArrayRef> = record_batch
+ .columns()
+ .iter()
+ .map(|array| {
+ let capacity = array.len();
+ let data = array.to_data();
+ let mut mutable = MutableArrayData::new(vec![&data], false, capacity);
Review Comment:
> It was necessary to have the CopyExec because FilterExec can pass through input batches in the case where the predicate evaluates to true for all rows and this is not safe in Comet because we re-use arrays in the ScanExec.
I wonder if you can use the machinery with unary_mut to know when it is safe to reuse the arrays and when a copy is required.
So that would look like the scan using something like `unary_mut`, which can check if there are other existing references or not.
For example, see the examples here https://docs.rs/arrow/latest/arrow/array/struct.PrimitiveArray.html#method.unary_mut ?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#issuecomment-2293924547
@mbutrovich you may be interested in reviewing this PR
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720282545
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,450 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn comet_filter_record_batch(
Review Comment:
Okay.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove merged PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "Dandandan (via GitHub)" <gi...@apache.org>.
Dandandan commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720161761
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
+ for i in 0..predicate.len() {
+ if !predicate.is_null(i) && predicate.value(i) {
+ sv.append_value(i as i32);
+ }
+ }
+ let sv = sv.finish();
+ // note that this does not unpack dictionary-encoded arrays
+ take_record_batch(record_batch, &sv)
Review Comment:
Probably a better way to copy is to use MutableArrayData, like done in concatenate:
https://docs.rs/arrow-select/52.2.0/src/arrow_select/concat.rs.html#180
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720275884
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,442 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::take_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::builder::Int32Builder;
+use arrow_array::{Array, BooleanArray};
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
+pub fn filter_record_batch(
+ record_batch: &RecordBatch,
+ predicate: &BooleanArray,
+) -> std::result::Result<RecordBatch, ArrowError> {
+ // turn predicate into selection vector
+ let mut sv = Int32Builder::with_capacity(predicate.true_count());
+ for i in 0..predicate.len() {
+ if !predicate.is_null(i) && predicate.value(i) {
+ sv.append_value(i as i32);
+ }
+ }
+ let sv = sv.finish();
+ // note that this does not unpack dictionary-encoded arrays
+ take_record_batch(record_batch, &sv)
Review Comment:
The goal of this PR is to no longer need to use CopyExec after a FilterExec (to avoid copying twice in some cases)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org
Re: [PR] perf: Remove redundant copying of batches after FilterExec [datafusion-comet]
Posted by "viirya (via GitHub)" <gi...@apache.org>.
viirya commented on code in PR #835:
URL: https://github.com/apache/datafusion-comet/pull/835#discussion_r1720279184
##########
native/core/src/execution/operators/filter.rs:
##########
@@ -0,0 +1,450 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::any::Any;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{ready, Context, Poll};
+
+use datafusion::physical_plan::{
+ metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet},
+ ColumnStatistics, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
+ PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
+};
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::{DataType, SchemaRef};
+use arrow::record_batch::RecordBatch;
+use arrow_array::{make_array, Array, ArrayRef, BooleanArray, RecordBatchOptions};
+use arrow_data::transform::MutableArrayData;
+use arrow_schema::ArrowError;
+use datafusion_common::cast::as_boolean_array;
+use datafusion_common::stats::Precision;
+use datafusion_common::{internal_err, plan_err, DataFusionError, Result};
+use datafusion_execution::TaskContext;
+use datafusion_expr::Operator;
+use datafusion_physical_expr::expressions::BinaryExpr;
+use datafusion_physical_expr::intervals::utils::check_support;
+use datafusion_physical_expr::utils::collect_columns;
+use datafusion_physical_expr::{
+ analyze, split_conjunction, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr,
+};
+
+use futures::stream::{Stream, StreamExt};
+use log::trace;
+
+/// This is a copy of DataFusion's FilterExec with one modification to ensure that input
+/// batches are never passed through unchanged. The changes are between the comments
+/// `BEGIN Comet change` and `END Comet change`.
+#[derive(Debug)]
+pub struct FilterExec {
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input plan
+ input: Arc<dyn ExecutionPlan>,
+ /// Execution metrics
+ metrics: ExecutionPlanMetricsSet,
+ /// Selectivity for statistics. 0 = no rows, 100 = all rows
+ default_selectivity: u8,
+ /// Properties equivalence properties, partitioning, etc.
+ cache: PlanProperties,
+}
+
+impl FilterExec {
+ /// Create a FilterExec on an input
+ pub fn try_new(
+ predicate: Arc<dyn PhysicalExpr>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ match predicate.data_type(input.schema().as_ref())? {
+ DataType::Boolean => {
+ let default_selectivity = 20;
+ let cache = Self::compute_properties(&input, &predicate, default_selectivity)?;
+ Ok(Self {
+ predicate,
+ input: Arc::clone(&input),
+ metrics: ExecutionPlanMetricsSet::new(),
+ default_selectivity,
+ cache,
+ })
+ }
+ other => {
+ plan_err!("Filter predicate must return BOOLEAN values, got {other:?}")
+ }
+ }
+ }
+
+ pub fn with_default_selectivity(
+ mut self,
+ default_selectivity: u8,
+ ) -> Result<Self, DataFusionError> {
+ if default_selectivity > 100 {
+ return plan_err!(
+ "Default filter selectivity value needs to be less than or equal to 100"
+ );
+ }
+ self.default_selectivity = default_selectivity;
+ Ok(self)
+ }
+
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ pub fn predicate(&self) -> &Arc<dyn PhysicalExpr> {
+ &self.predicate
+ }
+
+ /// The input plan
+ pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
+ &self.input
+ }
+
+ /// The default selectivity
+ pub fn default_selectivity(&self) -> u8 {
+ self.default_selectivity
+ }
+
+ /// Calculates `Statistics` for `FilterExec`, by applying selectivity (either default, or estimated) to input statistics.
+ fn statistics_helper(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<Statistics> {
+ let input_stats = input.statistics()?;
+ let schema = input.schema();
+ if !check_support(predicate, &schema) {
+ let selectivity = default_selectivity as f64 / 100.0;
+ let mut stats = input_stats.into_inexact();
+ stats.num_rows = stats.num_rows.with_estimated_selectivity(selectivity);
+ stats.total_byte_size = stats
+ .total_byte_size
+ .with_estimated_selectivity(selectivity);
+ return Ok(stats);
+ }
+
+ let num_rows = input_stats.num_rows;
+ let total_byte_size = input_stats.total_byte_size;
+ let input_analysis_ctx =
+ AnalysisContext::try_from_statistics(&input.schema(), &input_stats.column_statistics)?;
+
+ let analysis_ctx = analyze(predicate, input_analysis_ctx, &schema)?;
+
+ // Estimate (inexact) selectivity of predicate
+ let selectivity = analysis_ctx.selectivity.unwrap_or(1.0);
+ let num_rows = num_rows.with_estimated_selectivity(selectivity);
+ let total_byte_size = total_byte_size.with_estimated_selectivity(selectivity);
+
+ let column_statistics =
+ collect_new_statistics(&input_stats.column_statistics, analysis_ctx.boundaries);
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
+
+ fn extend_constants(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ ) -> Vec<ConstExpr> {
+ let mut res_constants = Vec::new();
+ let input_eqs = input.equivalence_properties();
+
+ let conjunctions = split_conjunction(predicate);
+ for conjunction in conjunctions {
+ if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>() {
+ if binary.op() == &Operator::Eq {
+ // Filter evaluates to single value for all partitions
+ if input_eqs.is_expr_constant(binary.left()) {
+ res_constants
+ .push(ConstExpr::from(binary.right()).with_across_partitions(true))
+ } else if input_eqs.is_expr_constant(binary.right()) {
+ res_constants
+ .push(ConstExpr::from(binary.left()).with_across_partitions(true))
+ }
+ }
+ }
+ }
+ res_constants
+ }
+ /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
+ fn compute_properties(
+ input: &Arc<dyn ExecutionPlan>,
+ predicate: &Arc<dyn PhysicalExpr>,
+ default_selectivity: u8,
+ ) -> Result<PlanProperties> {
+ // Combine the equal predicates with the input equivalence properties
+ // to construct the equivalence properties:
+ let stats = Self::statistics_helper(input, predicate, default_selectivity)?;
+ let mut eq_properties = input.equivalence_properties().clone();
+ let (equal_pairs, _) = collect_columns_from_predicate(predicate);
+ for (lhs, rhs) in equal_pairs {
+ eq_properties.add_equal_conditions(lhs, rhs)?
+ }
+ // Add the columns that have only one viable value (singleton) after
+ // filtering to constants.
+ let constants = collect_columns(predicate)
+ .into_iter()
+ .filter(|column| stats.column_statistics[column.index()].is_singleton())
+ .map(|column| {
+ let expr = Arc::new(column) as _;
+ ConstExpr::new(expr).with_across_partitions(true)
+ });
+ // this is for statistics
+ eq_properties = eq_properties.add_constants(constants);
+ // this is for logical constant (for example: a = '1', then a could be marked as a constant)
+ // to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
+ eq_properties = eq_properties.add_constants(Self::extend_constants(input, predicate));
+ Ok(PlanProperties::new(
+ eq_properties,
+ input.output_partitioning().clone(), // Output Partitioning
+ input.execution_mode(), // Execution Mode
+ ))
+ }
+}
+
+impl DisplayAs for FilterExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match t {
+ DisplayFormatType::Default | DisplayFormatType::Verbose => {
+ write!(f, "FilterExec: {}", self.predicate)
+ }
+ }
+ }
+}
+
+impl ExecutionPlan for FilterExec {
+ fn name(&self) -> &'static str {
+ "FilterExec"
+ }
+
+ /// Return a reference to Any that can be used for downcasting
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn properties(&self) -> &PlanProperties {
+ &self.cache
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ // tell optimizer this operator doesn't reorder its input
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ mut children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ FilterExec::try_new(Arc::clone(&self.predicate), children.swap_remove(0))
+ .and_then(|e| {
+ let selectivity = e.default_selectivity();
+ e.with_default_selectivity(selectivity)
+ })
+ .map(|e| Arc::new(e) as _)
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ trace!(
+ "Start FilterExec::execute for partition {} of context session_id {} and task_id {:?}",
+ partition,
+ context.session_id(),
+ context.task_id()
+ );
+ let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
+ Ok(Box::pin(FilterExecStream {
+ schema: self.input.schema(),
+ predicate: Arc::clone(&self.predicate),
+ input: self.input.execute(partition, context)?,
+ baseline_metrics,
+ }))
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ Some(self.metrics.clone_inner())
+ }
+
+ /// The output statistics of a filtering operation can be estimated if the
+ /// predicate's selectivity value can be determined for the incoming data.
+ fn statistics(&self) -> Result<Statistics> {
+ Self::statistics_helper(&self.input, self.predicate(), self.default_selectivity)
+ }
+}
+
+/// This function ensures that all bounds in the `ExprBoundaries` vector are
+/// converted to closed bounds. If a lower/upper bound is initially open, it
+/// is adjusted by using the next/previous value for its data type to convert
+/// it into a closed bound.
+fn collect_new_statistics(
+ input_column_stats: &[ColumnStatistics],
+ analysis_boundaries: Vec<ExprBoundaries>,
+) -> Vec<ColumnStatistics> {
+ analysis_boundaries
+ .into_iter()
+ .enumerate()
+ .map(
+ |(
+ idx,
+ ExprBoundaries {
+ interval,
+ distinct_count,
+ ..
+ },
+ )| {
+ let (lower, upper) = interval.into_bounds();
+ let (min_value, max_value) = if lower.eq(&upper) {
+ (Precision::Exact(lower), Precision::Exact(upper))
+ } else {
+ (Precision::Inexact(lower), Precision::Inexact(upper))
+ };
+ ColumnStatistics {
+ null_count: input_column_stats[idx].null_count.clone().to_inexact(),
+ max_value,
+ min_value,
+ distinct_count: distinct_count.to_inexact(),
+ }
+ },
+ )
+ .collect()
+}
+
+/// The FilterExec streams wraps the input iterator and applies the predicate expression to
+/// determine which rows to include in its output batches
+struct FilterExecStream {
+ /// Output schema, which is the same as the input schema for this operator
+ schema: SchemaRef,
+ /// The expression to filter on. This expression must evaluate to a boolean value.
+ predicate: Arc<dyn PhysicalExpr>,
+ /// The input partition to filter.
+ input: SendableRecordBatchStream,
+ /// runtime metrics recording
+ baseline_metrics: BaselineMetrics,
+}
+
+pub(crate) fn batch_filter(
+ batch: &RecordBatch,
+ predicate: &Arc<dyn PhysicalExpr>,
+) -> Result<RecordBatch> {
+ predicate
+ .evaluate(batch)
+ .and_then(|v| v.into_array(batch.num_rows()))
+ .and_then(|array| {
+ Ok(match as_boolean_array(&array) {
+ // apply filter array to record batch
+ Ok(filter_array) => comet_filter_record_batch(batch, filter_array)?,
+ Err(_) => {
+ return internal_err!("Cannot create filter_array from non-boolean predicates");
+ }
+ })
+ })
+}
+
+// BEGIN Comet changes
Review Comment:
```suggestion
// BEGIN Comet changes
// `FilterExec` could modify input batch or return input batch without change. Instead of always
// adding `CopyExec` on top of it, we only copy input batch for the special case.
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org