This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new ede5598d55 Improve `CommonSubexprEliminate`  identifier management 
(10% faster planning) (#10473)
ede5598d55 is described below

commit ede5598d5593a97ebb016b4e6e40653bf3e29fb8
Author: Peter Toth <[email protected]>
AuthorDate: Mon Jun 24 22:27:16 2024 +0200

    Improve `CommonSubexprEliminate`  identifier management (10% faster 
planning) (#10473)
    
    * implement hash based CSE identifier
    
    * move `is_volatile()` check out of visitor
    
    * fix comments
    
    * add transformed asserts when `rewrite_expr` is called
    
    * update MSRV to 1.76
    
    * cleanup
    
    * better comments regarding volatile and short circuiting expressions
    
    * address review comments
---
 Cargo.toml                                         |   2 +-
 datafusion-cli/Cargo.toml                          |   2 +-
 datafusion/common/src/hash_utils.rs                |   2 +-
 datafusion/core/Cargo.toml                         |   2 +-
 datafusion/expr/src/expr.rs                        | 173 +++-
 .../optimizer/src/common_subexpr_eliminate.rs      | 890 +++++++++++++--------
 datafusion/proto-common/Cargo.toml                 |   2 +-
 datafusion/proto-common/gen/Cargo.toml             |   2 +-
 datafusion/proto/Cargo.toml                        |   2 +-
 datafusion/proto/gen/Cargo.toml                    |   2 +-
 datafusion/substrait/Cargo.toml                    |   2 +-
 11 files changed, 723 insertions(+), 358 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index aa1ba1f214..6a6928e25b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,7 +52,7 @@ homepage = "https://datafusion.apache.org";
 license = "Apache-2.0"
 readme = "README.md"
 repository = "https://github.com/apache/datafusion";
-rust-version = "1.75"
+rust-version = "1.76"
 version = "39.0.0"
 
 [workspace.dependencies]
diff --git a/datafusion-cli/Cargo.toml b/datafusion-cli/Cargo.toml
index 8f4b3cd81f..8578476ed4 100644
--- a/datafusion-cli/Cargo.toml
+++ b/datafusion-cli/Cargo.toml
@@ -26,7 +26,7 @@ license = "Apache-2.0"
 homepage = "https://datafusion.apache.org";
 repository = "https://github.com/apache/datafusion";
 # Specify MSRV here as `cargo msrv` doesn't support workspace version
-rust-version = "1.75"
+rust-version = "1.76"
 readme = "README.md"
 
 [dependencies]
diff --git a/datafusion/common/src/hash_utils.rs 
b/datafusion/common/src/hash_utils.rs
index 7eecdec8ab..c972536c4d 100644
--- a/datafusion/common/src/hash_utils.rs
+++ b/datafusion/common/src/hash_utils.rs
@@ -36,7 +36,7 @@ use crate::error::{Result, _internal_err};
 
 // Combines two hashes into one hash
 #[inline]
-fn combine_hashes(l: u64, r: u64) -> u64 {
+pub fn combine_hashes(l: u64, r: u64) -> u64 {
     let hash = (17 * 37u64).wrapping_add(l);
     hash.wrapping_mul(37).wrapping_add(r)
 }
diff --git a/datafusion/core/Cargo.toml b/datafusion/core/Cargo.toml
index 45617d88dc..532ca8fde9 100644
--- a/datafusion/core/Cargo.toml
+++ b/datafusion/core/Cargo.toml
@@ -30,7 +30,7 @@ authors = { workspace = true }
 # Specify MSRV here as `cargo msrv` doesn't support workspace version and 
fails with
 # "Unable to find key 'package.rust-version' (or 'package.metadata.msrv') in 
'arrow-datafusion/Cargo.toml'"
 # https://github.com/foresterre/cargo-msrv/issues/590
-rust-version = "1.75"
+rust-version = "1.76"
 
 [lints]
 workspace = true
diff --git a/datafusion/expr/src/expr.rs b/datafusion/expr/src/expr.rs
index 409ccf7b47..b1debf24e7 100644
--- a/datafusion/expr/src/expr.rs
+++ b/datafusion/expr/src/expr.rs
@@ -19,7 +19,8 @@
 
 use std::collections::HashSet;
 use std::fmt::{self, Display, Formatter, Write};
-use std::hash::Hash;
+use std::hash::{Hash, Hasher};
+use std::mem;
 use std::str::FromStr;
 use std::sync::Arc;
 
@@ -1461,6 +1462,176 @@ impl Expr {
             | Expr::Placeholder(..) => false,
         }
     }
+
+    /// Hashes the direct content of an `Expr` without recursing into its 
children.
+    ///
+    /// This method is useful to incrementally compute hashes, such as  in
+    /// `CommonSubexprEliminate` which builds a deep hash of a node and its 
descendants
+    /// during the bottom-up phase of the first traversal and so avoid 
computing the hash
+    /// of the node and then the hash of its descendants separately.
+    ///
+    /// If a node doesn't have any children then this method is similar to 
`.hash()`, but
+    /// not necessarily returns the same value.
+    ///
+    /// As it is pretty easy to forget changing this method when `Expr` 
changes the
+    /// implementation doesn't use wildcard patterns (`..`, `_`) to catch 
changes
+    /// compile time.
+    pub fn hash_node<H: Hasher>(&self, hasher: &mut H) {
+        mem::discriminant(self).hash(hasher);
+        match self {
+            Expr::Alias(Alias {
+                expr: _expr,
+                relation,
+                name,
+            }) => {
+                relation.hash(hasher);
+                name.hash(hasher);
+            }
+            Expr::Column(column) => {
+                column.hash(hasher);
+            }
+            Expr::ScalarVariable(data_type, name) => {
+                data_type.hash(hasher);
+                name.hash(hasher);
+            }
+            Expr::Literal(scalar_value) => {
+                scalar_value.hash(hasher);
+            }
+            Expr::BinaryExpr(BinaryExpr {
+                left: _left,
+                op,
+                right: _right,
+            }) => {
+                op.hash(hasher);
+            }
+            Expr::Like(Like {
+                negated,
+                expr: _expr,
+                pattern: _pattern,
+                escape_char,
+                case_insensitive,
+            })
+            | Expr::SimilarTo(Like {
+                negated,
+                expr: _expr,
+                pattern: _pattern,
+                escape_char,
+                case_insensitive,
+            }) => {
+                negated.hash(hasher);
+                escape_char.hash(hasher);
+                case_insensitive.hash(hasher);
+            }
+            Expr::Not(_expr)
+            | Expr::IsNotNull(_expr)
+            | Expr::IsNull(_expr)
+            | Expr::IsTrue(_expr)
+            | Expr::IsFalse(_expr)
+            | Expr::IsUnknown(_expr)
+            | Expr::IsNotTrue(_expr)
+            | Expr::IsNotFalse(_expr)
+            | Expr::IsNotUnknown(_expr)
+            | Expr::Negative(_expr) => {}
+            Expr::Between(Between {
+                expr: _expr,
+                negated,
+                low: _low,
+                high: _high,
+            }) => {
+                negated.hash(hasher);
+            }
+            Expr::Case(Case {
+                expr: _expr,
+                when_then_expr: _when_then_expr,
+                else_expr: _else_expr,
+            }) => {}
+            Expr::Cast(Cast {
+                expr: _expr,
+                data_type,
+            })
+            | Expr::TryCast(TryCast {
+                expr: _expr,
+                data_type,
+            }) => {
+                data_type.hash(hasher);
+            }
+            Expr::Sort(Sort {
+                expr: _expr,
+                asc,
+                nulls_first,
+            }) => {
+                asc.hash(hasher);
+                nulls_first.hash(hasher);
+            }
+            Expr::ScalarFunction(ScalarFunction { func, args: _args }) => {
+                func.hash(hasher);
+            }
+            Expr::AggregateFunction(AggregateFunction {
+                func_def,
+                args: _args,
+                distinct,
+                filter: _filter,
+                order_by: _order_by,
+                null_treatment,
+            }) => {
+                func_def.hash(hasher);
+                distinct.hash(hasher);
+                null_treatment.hash(hasher);
+            }
+            Expr::WindowFunction(WindowFunction {
+                fun,
+                args: _args,
+                partition_by: _partition_by,
+                order_by: _order_by,
+                window_frame,
+                null_treatment,
+            }) => {
+                fun.hash(hasher);
+                window_frame.hash(hasher);
+                null_treatment.hash(hasher);
+            }
+            Expr::InList(InList {
+                expr: _expr,
+                list: _list,
+                negated,
+            }) => {
+                negated.hash(hasher);
+            }
+            Expr::Exists(Exists { subquery, negated }) => {
+                subquery.hash(hasher);
+                negated.hash(hasher);
+            }
+            Expr::InSubquery(InSubquery {
+                expr: _expr,
+                subquery,
+                negated,
+            }) => {
+                subquery.hash(hasher);
+                negated.hash(hasher);
+            }
+            Expr::ScalarSubquery(subquery) => {
+                subquery.hash(hasher);
+            }
+            Expr::Wildcard { qualifier } => {
+                qualifier.hash(hasher);
+            }
+            Expr::GroupingSet(grouping_set) => {
+                mem::discriminant(grouping_set).hash(hasher);
+                match grouping_set {
+                    GroupingSet::Rollup(_exprs) | GroupingSet::Cube(_exprs) => 
{}
+                    GroupingSet::GroupingSets(_exprs) => {}
+                }
+            }
+            Expr::Placeholder(place_holder) => {
+                place_holder.hash(hasher);
+            }
+            Expr::OuterReferenceColumn(data_type, column) => {
+                data_type.hash(hasher);
+                column.hash(hasher);
+            }
+            Expr::Unnest(Unnest { expr: _expr }) => {}
+        };
+    }
 }
 
 // modifies expr if it is a placeholder with datatype of right
diff --git a/datafusion/optimizer/src/common_subexpr_eliminate.rs 
b/datafusion/optimizer/src/common_subexpr_eliminate.rs
index 66c23fdced..e760845e04 100644
--- a/datafusion/optimizer/src/common_subexpr_eliminate.rs
+++ b/datafusion/optimizer/src/common_subexpr_eliminate.rs
@@ -18,6 +18,7 @@
 //! [`CommonSubexprEliminate`] to avoid redundant computation of common 
sub-expressions
 
 use std::collections::{BTreeSet, HashMap};
+use std::hash::{BuildHasher, Hash, Hasher, RandomState};
 use std::sync::Arc;
 
 use crate::{OptimizerConfig, OptimizerRule};
@@ -25,11 +26,12 @@ use crate::{OptimizerConfig, OptimizerRule};
 use crate::optimizer::ApplyOrder;
 use crate::utils::NamePreserver;
 use datafusion_common::alias::AliasGenerator;
+use datafusion_common::hash_utils::combine_hashes;
 use datafusion_common::tree_node::{
     Transformed, TreeNode, TreeNodeRecursion, TreeNodeRewriter, 
TreeNodeVisitor,
 };
 use datafusion_common::{
-    internal_datafusion_err, qualified_name, Column, DFSchema, Result,
+    internal_datafusion_err, qualified_name, Column, DFSchema, DFSchemaRef, 
Result,
 };
 use datafusion_expr::expr::Alias;
 use datafusion_expr::logical_plan::tree_node::unwrap_arc;
@@ -43,18 +45,37 @@ const CSE_PREFIX: &str = "__common_expr";
 
 /// Identifier that represents a subexpression tree.
 ///
-/// Note that the current implementation contains:
-/// - the `Display` of an expression (a `String`) and
-/// - the identifiers of the childrens of the expression
-/// concatenated.
-///
-/// An identifier should (ideally) be able to "hash", "accumulate", "equal" 
and "have no
-/// collision (as low as possible)"
-///
-/// Since an identifier is likely to be copied many times, it is better that 
an identifier
-/// is small or "copy". otherwise some kinds of reference count is needed. 
String
-/// description here is not such a good choose.
-type Identifier = String;
+/// This identifier is designed to be efficient and  "hash", "accumulate", 
"equal" and
+/// "have no collision (as low as possible)"
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+struct Identifier<'n> {
+    // Hash of `expr` built up incrementally during the first, visiting 
traversal, but its
+    // value is not necessarily equal to `expr.hash()`.
+    hash: u64,
+    expr: &'n Expr,
+}
+
+impl<'n> Identifier<'n> {
+    fn new(expr: &'n Expr, random_state: &RandomState) -> Self {
+        let mut hasher = random_state.build_hasher();
+        expr.hash_node(&mut hasher);
+        let hash = hasher.finish();
+        Self { hash, expr }
+    }
+
+    fn combine(mut self, other: Option<Self>) -> Self {
+        other.map_or(self, |other_id| {
+            self.hash = combine_hashes(self.hash, other_id.hash);
+            self
+        })
+    }
+}
+
+impl Hash for Identifier<'_> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        state.write_u64(self.hash);
+    }
+}
 
 /// A cache that contains the postorder index and the identifier of expression 
tree nodes
 /// by the preorder index of the nodes.
@@ -83,14 +104,14 @@ type Identifier = String;
 ///   (0, "b")
 /// ]
 /// ```
-type IdArray = Vec<(usize, Identifier)>;
+type IdArray<'n> = Vec<(usize, Option<Identifier<'n>>)>;
 
 /// A map that contains the number of occurrences of expressions by their 
identifiers.
-type ExprStats = HashMap<Identifier, usize>;
+type ExprStats<'n> = HashMap<Identifier<'n>, usize>;
 
 /// A map that contains the common expressions and their alias extracted 
during the
 /// second, rewriting traversal.
-type CommonExprs = IndexMap<Identifier, (Expr, String)>;
+type CommonExprs<'n> = IndexMap<Identifier<'n>, (Expr, String)>;
 
 /// Performs Common Sub-expression Elimination optimization.
 ///
@@ -118,21 +139,86 @@ type CommonExprs = IndexMap<Identifier, (Expr, String)>;
 /// ProjectionExec(exprs=[extract (day from new_col), extract (year from 
new_col)]) <-- reuse here
 ///   ProjectionExec(exprs=[to_date(c1) as new_col]) <-- compute to_date once
 /// ```
-pub struct CommonSubexprEliminate {}
+pub struct CommonSubexprEliminate {
+    random_state: RandomState,
+}
 
 impl CommonSubexprEliminate {
+    pub fn new() -> Self {
+        Self {
+            random_state: RandomState::new(),
+        }
+    }
+
+    /// Returns the identifier list for each element in `exprs` and a flag to 
indicate if
+    /// rewrite phase of CSE make sense.
+    ///
+    /// Returns and array with 1 element for each input expr in `exprs`
+    ///
+    /// Each element is itself the result of 
[`CommonSubexprEliminate::expr_to_identifier`] for that expr
+    /// (e.g. the identifiers for each node in the tree)
+    fn to_arrays<'n>(
+        &self,
+        exprs: &'n [Expr],
+        expr_stats: &mut ExprStats<'n>,
+        expr_mask: ExprMask,
+    ) -> Result<(bool, Vec<IdArray<'n>>)> {
+        let mut found_common = false;
+        exprs
+            .iter()
+            .map(|e| {
+                let mut id_array = vec![];
+                self.expr_to_identifier(e, expr_stats, &mut id_array, 
expr_mask)
+                    .map(|fc| {
+                        found_common |= fc;
+
+                        id_array
+                    })
+            })
+            .collect::<Result<Vec<_>>>()
+            .map(|id_arrays| (found_common, id_arrays))
+    }
+
+    /// Add an identifier to `id_array` for every subexpression in this tree.
+    fn expr_to_identifier<'n>(
+        &self,
+        expr: &'n Expr,
+        expr_stats: &mut ExprStats<'n>,
+        id_array: &mut IdArray<'n>,
+        expr_mask: ExprMask,
+    ) -> Result<bool> {
+        // Don't consider volatile expressions for CSE.
+        Ok(if expr.is_volatile()? {
+            false
+        } else {
+            let mut visitor = ExprIdentifierVisitor {
+                expr_stats,
+                id_array,
+                visit_stack: vec![],
+                down_index: 0,
+                up_index: 0,
+                expr_mask,
+                random_state: &self.random_state,
+                found_common: false,
+            };
+            expr.visit(&mut visitor)?;
+
+            visitor.found_common
+        })
+    }
+
     /// Rewrites `exprs_list` with common sub-expressions replaced with a new
     /// column.
     ///
     /// `common_exprs` is updated with any sub expressions that were replaced.
     ///
     /// Returns the rewritten expressions
-    fn rewrite_exprs_list(
+    fn rewrite_exprs_list<'n>(
         &self,
         exprs_list: Vec<Vec<Expr>>,
-        arrays_list: &[&[IdArray]],
-        expr_stats: &ExprStats,
-        common_exprs: &mut CommonExprs,
+        arrays_list: Vec<Vec<IdArray<'n>>>,
+        expr_stats: &ExprStats<'n>,
+        common_exprs: &mut CommonExprs<'n>,
         alias_generator: &AliasGenerator,
     ) -> Result<Transformed<Vec<Vec<Expr>>>> {
         let mut transformed = false;
@@ -175,7 +261,7 @@ impl CommonSubexprEliminate {
     fn rewrite_expr(
         &self,
         exprs_list: Vec<Vec<Expr>>,
-        arrays_list: &[&[IdArray]],
+        arrays_list: Vec<Vec<IdArray>>,
         input: LogicalPlan,
         expr_stats: &ExprStats,
         config: &dyn OptimizerConfig,
@@ -275,68 +361,95 @@ impl CommonSubexprEliminate {
         config: &dyn OptimizerConfig,
     ) -> Result<Transformed<LogicalPlan>> {
         // collect all window expressions from any number of LogicalPlanWindow
-        let ConsecutiveWindowExprs {
-            window_exprs,
-            arrays_per_window,
-            expr_stats,
-            plan,
-        } = ConsecutiveWindowExprs::try_new(window)?;
+        let (mut window_exprs, mut window_schemas, mut plan) =
+            get_consecutive_window_exprs(window);
 
-        let arrays_per_window = arrays_per_window
+        let mut found_common = false;
+        let mut expr_stats = ExprStats::new();
+        let arrays_per_window = window_exprs
             .iter()
-            .map(|arrays| arrays.as_slice())
-            .collect::<Vec<_>>();
+            .map(|window_expr| {
+                self.to_arrays(window_expr, &mut expr_stats, ExprMask::Normal)
+                    .map(|(fc, id_arrays)| {
+                        found_common |= fc;
 
-        // save the original names
-        let name_preserver = NamePreserver::new(&plan);
-        let mut saved_names = window_exprs
-            .iter()
-            .map(|exprs| {
-                exprs
-                    .iter()
-                    .map(|expr| name_preserver.save(expr))
-                    .collect::<Result<Vec<_>>>()
+                        id_arrays
+                    })
             })
             .collect::<Result<Vec<_>>>()?;
 
-        assert_eq!(window_exprs.len(), arrays_per_window.len());
-        let num_window_exprs = window_exprs.len();
-        let rewritten_window_exprs = self.rewrite_expr(
-            window_exprs,
-            &arrays_per_window,
-            plan,
-            &expr_stats,
-            config,
-        )?;
-        let transformed = rewritten_window_exprs.transformed;
+        if found_common {
+            // save the original names
+            let name_preserver = NamePreserver::new(&plan);
+            let mut saved_names = window_exprs
+                .iter()
+                .map(|exprs| {
+                    exprs
+                        .iter()
+                        .map(|expr| name_preserver.save(expr))
+                        .collect::<Result<Vec<_>>>()
+                })
+                .collect::<Result<Vec<_>>>()?;
 
-        let (mut new_expr, new_input) = rewritten_window_exprs.data;
+            assert_eq!(window_exprs.len(), arrays_per_window.len());
+            let num_window_exprs = window_exprs.len();
+            let rewritten_window_exprs = self.rewrite_expr(
+                // Must clone as Identifiers use references to original 
expressions so we
+                // have to keep the original expressions intact.
+                window_exprs.clone(),
+                arrays_per_window,
+                plan,
+                &expr_stats,
+                config,
+            )?;
+            let transformed = rewritten_window_exprs.transformed;
+            assert!(transformed);
 
-        let mut plan = new_input;
+            let (mut new_expr, new_input) = rewritten_window_exprs.data;
 
-        // Construct consecutive window operator, with their corresponding new
-        // window expressions.
-        //
-        // Note this iterates over, `new_expr` and `saved_names` which are the
-        // same length, in reverse order
-        assert_eq!(num_window_exprs, new_expr.len());
-        assert_eq!(num_window_exprs, saved_names.len());
-        while let (Some(new_window_expr), Some(saved_names)) =
-            (new_expr.pop(), saved_names.pop())
-        {
-            assert_eq!(new_window_expr.len(), saved_names.len());
+            let mut plan = new_input;
 
-            // Rename re-written window expressions with original name, to
-            // preserve the output schema
-            let new_window_expr = new_window_expr
-                .into_iter()
-                .zip(saved_names.into_iter())
-                .map(|(new_window_expr, saved_name)| 
saved_name.restore(new_window_expr))
-                .collect::<Result<Vec<_>>>()?;
-            plan = LogicalPlan::Window(Window::try_new(new_window_expr, 
Arc::new(plan))?);
-        }
+            // Construct consecutive window operator, with their corresponding 
new
+            // window expressions.
+            //
+            // Note this iterates over, `new_expr` and `saved_names` which are 
the
+            // same length, in reverse order
+            assert_eq!(num_window_exprs, new_expr.len());
+            assert_eq!(num_window_exprs, saved_names.len());
+            while let (Some(new_window_expr), Some(saved_names)) =
+                (new_expr.pop(), saved_names.pop())
+            {
+                assert_eq!(new_window_expr.len(), saved_names.len());
+
+                // Rename re-written window expressions with original name, to
+                // preserve the output schema
+                let new_window_expr = new_window_expr
+                    .into_iter()
+                    .zip(saved_names.into_iter())
+                    .map(|(new_window_expr, saved_name)| {
+                        saved_name.restore(new_window_expr)
+                    })
+                    .collect::<Result<Vec<_>>>()?;
+                plan = LogicalPlan::Window(Window::try_new(
+                    new_window_expr,
+                    Arc::new(plan),
+                )?);
+            }
 
-        Ok(Transformed::new_transformed(plan, transformed))
+            Ok(Transformed::new_transformed(plan, transformed))
+        } else {
+            while let (Some(window_expr), Some(schema)) =
+                (window_exprs.pop(), window_schemas.pop())
+            {
+                plan = LogicalPlan::Window(Window {
+                    input: Arc::new(plan),
+                    window_expr,
+                    schema,
+                });
+            }
+
+            Ok(Transformed::no(plan))
+        }
     }
 
     fn try_optimize_aggregate(
@@ -351,56 +464,112 @@ impl CommonSubexprEliminate {
             schema: orig_schema,
             ..
         } = aggregate;
-        let mut expr_stats = ExprStats::new();
-
         // track transformed information
         let mut transformed = false;
 
-        // rewrite inputs
-        let group_arrays = to_arrays(&group_expr, &mut expr_stats, 
ExprMask::Normal)?;
-        let aggr_arrays = to_arrays(&aggr_expr, &mut expr_stats, 
ExprMask::Normal)?;
-
         let name_perserver = NamePreserver::new_for_projection();
         let saved_names = aggr_expr
             .iter()
             .map(|expr| name_perserver.save(expr))
             .collect::<Result<Vec<_>>>()?;
 
-        // rewrite both group exprs and aggr_expr
-        let rewritten = self.rewrite_expr(
-            vec![group_expr, aggr_expr],
-            &[&group_arrays, &aggr_arrays],
-            unwrap_arc(input),
-            &expr_stats,
-            config,
-        )?;
-        transformed |= rewritten.transformed;
-        let (mut new_expr, new_input) = rewritten.data;
-
-        // note the reversed pop order.
-        let new_aggr_expr = pop_expr(&mut new_expr)?;
-        let new_group_expr = pop_expr(&mut new_expr)?;
+        let mut expr_stats = ExprStats::new();
+        // rewrite inputs
+        let (group_found_common, group_arrays) =
+            self.to_arrays(&group_expr, &mut expr_stats, ExprMask::Normal)?;
+        let (aggr_found_common, aggr_arrays) =
+            self.to_arrays(&aggr_expr, &mut expr_stats, ExprMask::Normal)?;
+        let (new_aggr_expr, new_group_expr, new_input) =
+            if group_found_common || aggr_found_common {
+                // rewrite both group exprs and aggr_expr
+                let rewritten = self.rewrite_expr(
+                    // Must clone as Identifiers use references to original 
expressions so
+                    // we have to keep the original expressions intact.
+                    vec![group_expr.clone(), aggr_expr.clone()],
+                    vec![group_arrays, aggr_arrays],
+                    unwrap_arc(input),
+                    &expr_stats,
+                    config,
+                )?;
+                assert!(rewritten.transformed);
+                transformed |= rewritten.transformed;
+                let (mut new_expr, new_input) = rewritten.data;
+
+                // note the reversed pop order.
+                let new_aggr_expr = pop_expr(&mut new_expr)?;
+                let new_group_expr = pop_expr(&mut new_expr)?;
+
+                (new_aggr_expr, new_group_expr, Arc::new(new_input))
+            } else {
+                (aggr_expr, group_expr, input)
+            };
 
         // create potential projection on top
         let mut expr_stats = ExprStats::new();
-        let new_input_schema = Arc::clone(new_input.schema());
-        let aggr_arrays = to_arrays(
+        let (aggr_found_common, aggr_arrays) = self.to_arrays(
             &new_aggr_expr,
             &mut expr_stats,
             ExprMask::NormalAndAggregates,
         )?;
-        let mut common_exprs = IndexMap::new();
-        let mut rewritten_exprs = self.rewrite_exprs_list(
-            vec![new_aggr_expr.clone()],
-            &[&aggr_arrays],
-            &expr_stats,
-            &mut common_exprs,
-            &config.alias_generator(),
-        )?;
-        transformed |= rewritten_exprs.transformed;
-        let rewritten = pop_expr(&mut rewritten_exprs.data)?;
+        if aggr_found_common {
+            let mut common_exprs = CommonExprs::new();
+            let mut rewritten_exprs = self.rewrite_exprs_list(
+                // Must clone as Identifiers use references to original 
expressions so we
+                // have to keep the original expressions intact.
+                vec![new_aggr_expr.clone()],
+                vec![aggr_arrays],
+                &expr_stats,
+                &mut common_exprs,
+                &config.alias_generator(),
+            )?;
+            assert!(rewritten_exprs.transformed);
+            let rewritten = pop_expr(&mut rewritten_exprs.data)?;
+
+            assert!(!common_exprs.is_empty());
+            let mut agg_exprs = common_exprs
+                .into_values()
+                .map(|(expr, expr_alias)| expr.alias(expr_alias))
+                .collect::<Vec<_>>();
+
+            let new_input_schema = Arc::clone(new_input.schema());
+            let mut proj_exprs = vec![];
+            for expr in &new_group_expr {
+                extract_expressions(expr, &new_input_schema, &mut proj_exprs)?
+            }
+            for (expr_rewritten, expr_orig) in 
rewritten.into_iter().zip(new_aggr_expr) {
+                if expr_rewritten == expr_orig {
+                    if let Expr::Alias(Alias { expr, name, .. }) = 
expr_rewritten {
+                        agg_exprs.push(expr.alias(&name));
+                        proj_exprs.push(Expr::Column(Column::from_name(name)));
+                    } else {
+                        let expr_alias = 
config.alias_generator().next(CSE_PREFIX);
+                        let (qualifier, field) =
+                            expr_rewritten.to_field(&new_input_schema)?;
+                        let out_name = qualified_name(qualifier.as_ref(), 
field.name());
+
+                        agg_exprs.push(expr_rewritten.alias(&expr_alias));
+                        proj_exprs.push(
+                            
Expr::Column(Column::from_name(expr_alias)).alias(out_name),
+                        );
+                    }
+                } else {
+                    proj_exprs.push(expr_rewritten);
+                }
+            }
 
-        if common_exprs.is_empty() {
+            let agg = LogicalPlan::Aggregate(Aggregate::try_new(
+                new_input,
+                new_group_expr,
+                agg_exprs,
+            )?);
+
+            Projection::try_new(proj_exprs, Arc::new(agg))
+                .map(LogicalPlan::Projection)
+                .map(Transformed::yes)
+        } else {
+            // TODO: How exactly can the name or the schema change in this 
case?
+            //  In theory `new_aggr_expr` and `new_group_expr` are either the 
original expressions or they were crafted via `rewrite_expr()`, that keeps the 
original expression names.
+            //  If this is really needed can we have UT for it?
             // Alias aggregation expressions if they have changed
             let new_aggr_expr = new_aggr_expr
                 .into_iter()
@@ -409,57 +578,19 @@ impl CommonSubexprEliminate {
                 .collect::<Result<Vec<Expr>>>()?;
             // Since group_expr may have changed, schema may also. Use try_new 
method.
             let new_agg = if transformed {
-                Aggregate::try_new(Arc::new(new_input), new_group_expr, 
new_aggr_expr)?
+                Aggregate::try_new(new_input, new_group_expr, new_aggr_expr)?
             } else {
                 Aggregate::try_new_with_schema(
-                    Arc::new(new_input),
+                    new_input,
                     new_group_expr,
                     new_aggr_expr,
                     orig_schema,
                 )?
             };
             let new_agg = LogicalPlan::Aggregate(new_agg);
-            return Ok(Transformed::new_transformed(new_agg, transformed));
-        }
-        let mut agg_exprs = common_exprs
-            .into_values()
-            .map(|(expr, expr_alias)| expr.alias(expr_alias))
-            .collect::<Vec<_>>();
-
-        let mut proj_exprs = vec![];
-        for expr in &new_group_expr {
-            extract_expressions(expr, &new_input_schema, &mut proj_exprs)?
-        }
-        for (expr_rewritten, expr_orig) in 
rewritten.into_iter().zip(new_aggr_expr) {
-            if expr_rewritten == expr_orig {
-                if let Expr::Alias(Alias { expr, name, .. }) = expr_rewritten {
-                    agg_exprs.push(expr.alias(&name));
-                    proj_exprs.push(Expr::Column(Column::from_name(name)));
-                } else {
-                    let expr_alias = config.alias_generator().next(CSE_PREFIX);
-                    let (qualifier, field) =
-                        expr_rewritten.to_field(&new_input_schema)?;
-                    let out_name = qualified_name(qualifier.as_ref(), 
field.name());
-
-                    agg_exprs.push(expr_rewritten.alias(&expr_alias));
-                    proj_exprs.push(
-                        
Expr::Column(Column::from_name(expr_alias)).alias(out_name),
-                    );
-                }
-            } else {
-                proj_exprs.push(expr_rewritten);
-            }
-        }
 
-        let agg = LogicalPlan::Aggregate(Aggregate::try_new(
-            Arc::new(new_input),
-            new_group_expr,
-            agg_exprs,
-        )?);
-
-        Projection::try_new(proj_exprs, Arc::new(agg))
-            .map(LogicalPlan::Projection)
-            .map(Transformed::yes)
+            Ok(Transformed::new_transformed(new_agg, transformed))
+        }
     }
 
     /// Rewrites the expr list and input to remove common subexpressions
@@ -483,13 +614,27 @@ impl CommonSubexprEliminate {
         config: &dyn OptimizerConfig,
     ) -> Result<Transformed<(Vec<Expr>, LogicalPlan)>> {
         let mut expr_stats = ExprStats::new();
-        let arrays = to_arrays(&expr, &mut expr_stats, ExprMask::Normal)?;
-
-        self.rewrite_expr(vec![expr], &[&arrays], input, &expr_stats, config)?
-            .map_data(|(mut new_expr, new_input)| {
+        let (found_common, id_arrays) =
+            self.to_arrays(&expr, &mut expr_stats, ExprMask::Normal)?;
+
+        if found_common {
+            let rewritten = self.rewrite_expr(
+                // Must clone as Identifiers use references to original 
expressions so we
+                // have to keep the original expressions intact.
+                vec![expr.clone()],
+                vec![id_arrays],
+                input,
+                &expr_stats,
+                config,
+            )?;
+            assert!(rewritten.transformed);
+            rewritten.map_data(|(mut new_expr, new_input)| {
                 assert_eq!(new_expr.len(), 1);
                 Ok((new_expr.pop().unwrap(), new_input))
             })
+        } else {
+            Ok(Transformed::no((expr, input)))
+        }
     }
 }
 
@@ -507,7 +652,7 @@ impl CommonSubexprEliminate {
 /// ```
 ///
 /// Returns:
-/// *  `window_exprs`: `[a, b, c, d]`
+/// *  `window_exprs`: `[[a, b, c], [d]]`
 /// * InputPlan
 ///
 /// Consecutive window expressions may refer to same complex expression.
@@ -524,41 +669,24 @@ impl CommonSubexprEliminate {
 /// ```
 ///
 /// where, it is referred once by each `WindowAggr` (total of 2) in the plan.
-struct ConsecutiveWindowExprs {
-    window_exprs: Vec<Vec<Expr>>,
-    /// result of calling `to_arrays` on each set of window exprs
-    arrays_per_window: Vec<Vec<Vec<(usize, String)>>>,
-    expr_stats: ExprStats,
-    /// input plan to the window
-    plan: LogicalPlan,
-}
-
-impl ConsecutiveWindowExprs {
-    fn try_new(window: Window) -> Result<Self> {
-        let mut window_exprs = vec![];
-        let mut arrays_per_window = vec![];
-        let mut expr_stats = ExprStats::new();
-
-        let mut plan = LogicalPlan::Window(window);
-        while let LogicalPlan::Window(Window {
-            input, window_expr, ..
-        }) = plan
-        {
-            plan = unwrap_arc(input);
-
-            let arrays = to_arrays(&window_expr, &mut expr_stats, 
ExprMask::Normal)?;
-
-            window_exprs.push(window_expr);
-            arrays_per_window.push(arrays);
-        }
-
-        Ok(Self {
-            window_exprs,
-            arrays_per_window,
-            expr_stats,
-            plan,
-        })
+fn get_consecutive_window_exprs(
+    window: Window,
+) -> (Vec<Vec<Expr>>, Vec<DFSchemaRef>, LogicalPlan) {
+    let mut window_exprs = vec![];
+    let mut window_schemas = vec![];
+    let mut plan = LogicalPlan::Window(window);
+    while let LogicalPlan::Window(Window {
+        input,
+        window_expr,
+        schema,
+    }) = plan
+    {
+        window_exprs.push(window_expr);
+        window_schemas.push(schema);
+
+        plan = unwrap_arc(input);
     }
+    (window_exprs, window_schemas, plan)
 }
 
 impl OptimizerRule for CommonSubexprEliminate {
@@ -632,41 +760,12 @@ impl Default for CommonSubexprEliminate {
     }
 }
 
-impl CommonSubexprEliminate {
-    #[allow(missing_docs)]
-    pub fn new() -> Self {
-        Self {}
-    }
-}
-
 fn pop_expr(new_expr: &mut Vec<Vec<Expr>>) -> Result<Vec<Expr>> {
     new_expr
         .pop()
         .ok_or_else(|| internal_datafusion_err!("Failed to pop expression"))
 }
 
-/// Returns the identifier list for each element in  `exprs`
-///
-/// Returns and array with 1 element for each input expr in `exprs`
-///
-/// Each element is itself the result of [`expr_to_identifier`] for that expr
-/// (e.g. the identifiers for each node in the tree)
-fn to_arrays(
-    exprs: &[Expr],
-    expr_stats: &mut ExprStats,
-    expr_mask: ExprMask,
-) -> Result<Vec<IdArray>> {
-    exprs
-        .iter()
-        .map(|e| {
-            let mut id_array = vec![];
-            expr_to_identifier(e, expr_stats, &mut id_array, expr_mask)?;
-
-            Ok(id_array)
-        })
-        .collect()
-}
-
 /// Build the "intermediate" projection plan that evaluates the extracted 
common
 /// expressions.
 ///
@@ -790,45 +889,48 @@ impl ExprMask {
 ///
 /// `Expr` without sub-expr (column, literal etc.) will not have identifier
 /// because they should not be recognized as common sub-expr.
-struct ExprIdentifierVisitor<'a> {
+struct ExprIdentifierVisitor<'a, 'n> {
     // statistics of expressions
-    expr_stats: &'a mut ExprStats,
+    expr_stats: &'a mut ExprStats<'n>,
     // cache to speed up second traversal
-    id_array: &'a mut IdArray,
+    id_array: &'a mut IdArray<'n>,
     // inner states
-    visit_stack: Vec<VisitRecord>,
+    visit_stack: Vec<VisitRecord<'n>>,
     // preorder index, start from 0.
     down_index: usize,
     // postorder index, start from 0.
     up_index: usize,
     // which expression should be skipped?
     expr_mask: ExprMask,
+    // a `RandomState` to generate hashes during the first traversal
+    random_state: &'a RandomState,
+    // a flag to indicate that common expression found
+    found_common: bool,
 }
 
 /// Record item that used when traversing a expression tree.
-enum VisitRecord {
+enum VisitRecord<'n> {
     /// `usize` postorder index assigned in `f-down`(). Starts from 0.
     EnterMark(usize),
     /// the node's children were skipped => jump to f_up on same node
     JumpMark,
     /// Accumulated identifier of sub expression.
-    ExprItem(Identifier),
+    ExprItem(Identifier<'n>),
 }
 
-impl ExprIdentifierVisitor<'_> {
+impl<'n> ExprIdentifierVisitor<'_, 'n> {
     /// Find the first `EnterMark` in the stack, and accumulates every 
`ExprItem`
     /// before it.
-    fn pop_enter_mark(&mut self) -> Option<(usize, Identifier)> {
-        let mut desc = String::new();
+    fn pop_enter_mark(&mut self) -> Option<(usize, Option<Identifier<'n>>)> {
+        let mut expr_id = None;
 
         while let Some(item) = self.visit_stack.pop() {
             match item {
                 VisitRecord::EnterMark(idx) => {
-                    return Some((idx, desc));
+                    return Some((idx, expr_id));
                 }
                 VisitRecord::ExprItem(id) => {
-                    desc.push('|');
-                    desc.push_str(&id);
+                    expr_id = Some(id.combine(expr_id));
                 }
                 VisitRecord::JumpMark => return None,
             }
@@ -837,21 +939,22 @@ impl ExprIdentifierVisitor<'_> {
     }
 }
 
-impl<'n> TreeNodeVisitor<'n> for ExprIdentifierVisitor<'_> {
+impl<'n> TreeNodeVisitor<'n> for ExprIdentifierVisitor<'_, 'n> {
     type Node = Expr;
 
     fn f_down(&mut self, expr: &'n Expr) -> Result<TreeNodeRecursion> {
-        // related to https://github.com/apache/arrow-datafusion/issues/8814
-        // If the expr contain volatile expression or is a short-circuit 
expression, skip it.
-        // TODO: propagate is_volatile state bottom-up + consider non-volatile 
sub-expressions for CSE
+        // TODO: consider non-volatile sub-expressions for CSE
         // TODO: consider surely executed children of "short circuited"s for 
CSE
-        if expr.short_circuits() || expr.is_volatile()? {
+
+        // If an expression can short circuit its children then don't consider 
it for CSE
+        // (https://github.com/apache/arrow-datafusion/issues/8814).
+        if expr.short_circuits() {
             self.visit_stack.push(VisitRecord::JumpMark);
 
             return Ok(TreeNodeRecursion::Jump);
         }
 
-        self.id_array.push((0, "".to_string()));
+        self.id_array.push((0, None));
         self.visit_stack
             .push(VisitRecord::EnterMark(self.down_index));
         self.down_index += 1;
@@ -864,13 +967,16 @@ impl<'n> TreeNodeVisitor<'n> for 
ExprIdentifierVisitor<'_> {
             return Ok(TreeNodeRecursion::Continue);
         };
 
-        let expr_id = expr_identifier(expr, sub_expr_id);
+        let expr_id = Identifier::new(expr, 
self.random_state).combine(sub_expr_id);
 
         self.id_array[down_index].0 = self.up_index;
         if !self.expr_mask.ignores(expr) {
-            self.id_array[down_index].1.clone_from(&expr_id);
-            let count = self.expr_stats.entry(expr_id.clone()).or_insert(0);
+            self.id_array[down_index].1 = Some(expr_id);
+            let count = self.expr_stats.entry(expr_id).or_insert(0);
             *count += 1;
+            if *count > 1 {
+                self.found_common = true;
+            }
         }
         self.visit_stack.push(VisitRecord::ExprItem(expr_id));
         self.up_index += 1;
@@ -879,40 +985,17 @@ impl<'n> TreeNodeVisitor<'n> for 
ExprIdentifierVisitor<'_> {
     }
 }
 
-fn expr_identifier(expr: &Expr, sub_expr_identifier: Identifier) -> Identifier 
{
-    format!("{{{expr}{sub_expr_identifier}}}")
-}
-
-/// Go through an expression tree and generate identifier for every node in 
this tree.
-fn expr_to_identifier(
-    expr: &Expr,
-    expr_stats: &mut ExprStats,
-    id_array: &mut IdArray,
-    expr_mask: ExprMask,
-) -> Result<()> {
-    expr.visit(&mut ExprIdentifierVisitor {
-        expr_stats,
-        id_array,
-        visit_stack: vec![],
-        down_index: 0,
-        up_index: 0,
-        expr_mask,
-    })?;
-
-    Ok(())
-}
-
 /// Rewrite expression by replacing detected common sub-expression with
 /// the corresponding temporary column name. That column contains the
 /// evaluate result of replaced expression.
-struct CommonSubexprRewriter<'a> {
+struct CommonSubexprRewriter<'a, 'n> {
     // statistics of expressions
-    expr_stats: &'a ExprStats,
+    expr_stats: &'a ExprStats<'n>,
     // cache to speed up second traversal
-    id_array: &'a IdArray,
+    id_array: &'a IdArray<'n>,
     // common expression, that are replaced during the second traversal, are 
collected to
     // this map
-    common_exprs: &'a mut CommonExprs,
+    common_exprs: &'a mut CommonExprs<'n>,
     // preorder index, starts from 0.
     down_index: usize,
     // how many aliases have we seen so far
@@ -921,17 +1004,9 @@ struct CommonSubexprRewriter<'a> {
     alias_generator: &'a AliasGenerator,
 }
 
-impl TreeNodeRewriter for CommonSubexprRewriter<'_> {
+impl TreeNodeRewriter for CommonSubexprRewriter<'_, '_> {
     type Node = Expr;
 
-    fn f_up(&mut self, expr: Expr) -> Result<Transformed<Self::Node>> {
-        if matches!(expr, Expr::Alias(_)) {
-            self.alias_counter -= 1
-        }
-
-        Ok(Transformed::no(expr))
-    }
-
     fn f_down(&mut self, expr: Expr) -> Result<Transformed<Expr>> {
         if matches!(expr, Expr::Alias(_)) {
             self.alias_counter += 1;
@@ -940,33 +1015,32 @@ impl TreeNodeRewriter for CommonSubexprRewriter<'_> {
         // The `CommonSubexprRewriter` relies on `ExprIdentifierVisitor` to 
generate
         // the `id_array`, which records the expr's identifier used to rewrite 
expr. So if we
         // skip an expr in `ExprIdentifierVisitor`, we should skip it here, 
too.
-        if expr.short_circuits() || expr.is_volatile()? {
+        if expr.short_circuits() {
             return Ok(Transformed::new(expr, false, TreeNodeRecursion::Jump));
         }
 
-        let (up_index, expr_id) = &self.id_array[self.down_index];
+        let (up_index, expr_id) = self.id_array[self.down_index];
         self.down_index += 1;
 
         // skip `Expr`s without identifier (empty identifier).
-        if expr_id.is_empty() {
+        let Some(expr_id) = expr_id else {
             return Ok(Transformed::no(expr));
-        }
+        };
 
-        let count = self.expr_stats.get(expr_id).unwrap();
+        let count = self.expr_stats.get(&expr_id).unwrap();
         if *count > 1 {
             // step index to skip all sub-node (which has smaller series 
number).
             while self.down_index < self.id_array.len()
-                && self.id_array[self.down_index].0 < *up_index
+                && self.id_array[self.down_index].0 < up_index
             {
                 self.down_index += 1;
             }
 
             let expr_name = expr.display_name()?;
-            let (_, expr_alias) =
-                self.common_exprs.entry(expr_id.clone()).or_insert_with(|| {
-                    let expr_alias = self.alias_generator.next(CSE_PREFIX);
-                    (expr, expr_alias)
-                });
+            let (_, expr_alias) = 
self.common_exprs.entry(expr_id).or_insert_with(|| {
+                let expr_alias = self.alias_generator.next(CSE_PREFIX);
+                (expr, expr_alias)
+            });
 
             // alias the expressions without an `Alias` ancestor node
             let rewritten = if self.alias_counter > 0 {
@@ -981,37 +1055,49 @@ impl TreeNodeRewriter for CommonSubexprRewriter<'_> {
             Ok(Transformed::no(expr))
         }
     }
+
+    fn f_up(&mut self, expr: Expr) -> Result<Transformed<Self::Node>> {
+        if matches!(expr, Expr::Alias(_)) {
+            self.alias_counter -= 1
+        }
+
+        Ok(Transformed::no(expr))
+    }
 }
 
 /// Replace common sub-expression in `expr` with the corresponding temporary
 /// column name, updating `common_exprs` with any replaced expressions
-fn replace_common_expr(
+fn replace_common_expr<'n>(
     expr: Expr,
-    id_array: &IdArray,
-    expr_stats: &ExprStats,
-    common_exprs: &mut CommonExprs,
+    id_array: &IdArray<'n>,
+    expr_stats: &ExprStats<'n>,
+    common_exprs: &mut CommonExprs<'n>,
     alias_generator: &AliasGenerator,
 ) -> Result<Transformed<Expr>> {
-    expr.rewrite(&mut CommonSubexprRewriter {
-        expr_stats,
-        id_array,
-        common_exprs,
-        down_index: 0,
-        alias_counter: 0,
-        alias_generator,
-    })
+    if id_array.is_empty() {
+        Ok(Transformed::no(expr))
+    } else {
+        expr.rewrite(&mut CommonSubexprRewriter {
+            expr_stats,
+            id_array,
+            common_exprs,
+            down_index: 0,
+            alias_counter: 0,
+            alias_generator,
+        })
+    }
 }
 
 #[cfg(test)]
 mod test {
+    use std::collections::HashSet;
     use std::iter;
 
     use arrow::datatypes::{DataType, Field, Schema};
-
+    use datafusion_expr::expr::AggregateFunction;
     use datafusion_expr::logical_plan::{table_scan, JoinType};
-
     use datafusion_expr::{
-        grouping_set, AccumulatorFactoryFunction, AggregateUDF, Signature,
+        grouping_set, AccumulatorFactoryFunction, AggregateUDF, BinaryExpr, 
Signature,
         SimpleAggregateUDF, Volatility,
     };
     use datafusion_expr::{lit, logical_plan::builder::LogicalPlanBuilder};
@@ -1028,7 +1114,7 @@ mod test {
         config: Option<&dyn OptimizerConfig>,
     ) {
         assert_eq!(expected, format!("{plan:?}"), "Unexpected starting plan");
-        let optimizer = CommonSubexprEliminate {};
+        let optimizer = CommonSubexprEliminate::new();
         let default_config = OptimizerContext::new();
         let config = config.unwrap_or(&default_config);
         let optimized_plan = optimizer.rewrite(plan, config).unwrap();
@@ -1046,7 +1132,7 @@ mod test {
         plan: LogicalPlan,
         config: Option<&dyn OptimizerConfig>,
     ) {
-        let optimizer = CommonSubexprEliminate {};
+        let optimizer = CommonSubexprEliminate::new();
         let default_config = OptimizerContext::new();
         let config = config.unwrap_or(&default_config);
         let optimized_plan = optimizer.rewrite(plan, config).unwrap();
@@ -1058,51 +1144,147 @@ mod test {
 
     #[test]
     fn id_array_visitor() -> Result<()> {
-        let expr = ((sum(col("a") + lit(1))) - avg(col("c"))) * lit(2);
+        let optimizer = CommonSubexprEliminate::new();
+
+        let a_plus_1 = col("a") + lit(1);
+        let avg_c = avg(col("c"));
+        let sum_a_plus_1 = sum(a_plus_1);
+        let sum_a_plus_1_minus_avg_c = sum_a_plus_1 - avg_c;
+        let expr = sum_a_plus_1_minus_avg_c * lit(2);
+
+        let Expr::BinaryExpr(BinaryExpr {
+            left: sum_a_plus_1_minus_avg_c,
+            ..
+        }) = &expr
+        else {
+            panic!("Cannot extract subexpression reference")
+        };
+        let Expr::BinaryExpr(BinaryExpr {
+            left: sum_a_plus_1,
+            right: avg_c,
+            ..
+        }) = sum_a_plus_1_minus_avg_c.as_ref()
+        else {
+            panic!("Cannot extract subexpression reference")
+        };
+        let Expr::AggregateFunction(AggregateFunction {
+            args: a_plus_1_vec, ..
+        }) = sum_a_plus_1.as_ref()
+        else {
+            panic!("Cannot extract subexpression reference")
+        };
+        let a_plus_1 = &a_plus_1_vec.as_slice()[0];
 
         // skip aggregates
         let mut id_array = vec![];
-        expr_to_identifier(&expr, &mut HashMap::new(), &mut id_array, 
ExprMask::Normal)?;
+        optimizer.expr_to_identifier(
+            &expr,
+            &mut ExprStats::new(),
+            &mut id_array,
+            ExprMask::Normal,
+        )?;
+
+        // Collect distinct hashes and set them to 0 in `id_array`
+        fn collect_hashes(id_array: &mut IdArray) -> HashSet<u64> {
+            id_array
+                .iter_mut()
+                .flat_map(|(_, expr_id_option)| {
+                    expr_id_option.as_mut().map(|expr_id| {
+                        let hash = expr_id.hash;
+                        expr_id.hash = 0;
+                        hash
+                    })
+                })
+                .collect::<HashSet<_>>()
+        }
+
+        let hashes = collect_hashes(&mut id_array);
+        assert_eq!(hashes.len(), 3);
 
         let expected = vec![
-            (8, "{(sum(a + Int32(1)) - avg(c)) * Int32(2)|{Int32(2)}|{sum(a + 
Int32(1)) - avg(c)|{avg(c)|{c}}|{sum(a + Int32(1))|{a + 
Int32(1)|{Int32(1)}|{a}}}}}"),
-            (6, "{sum(a + Int32(1)) - avg(c)|{avg(c)|{c}}|{sum(a + 
Int32(1))|{a + Int32(1)|{Int32(1)}|{a}}}}"),
-            (3, ""),
-            (2, "{a + Int32(1)|{Int32(1)}|{a}}"),
-            (0, ""),
-            (1, ""),
-            (5, ""),
-            (4, ""),
-            (7, "")
-        ]
-        .into_iter()
-        .map(|(number, id)| (number, id.into()))
-        .collect::<Vec<_>>();
+            (
+                8,
+                Some(Identifier {
+                    hash: 0,
+                    expr: &expr,
+                }),
+            ),
+            (
+                6,
+                Some(Identifier {
+                    hash: 0,
+                    expr: sum_a_plus_1_minus_avg_c,
+                }),
+            ),
+            (3, None),
+            (
+                2,
+                Some(Identifier {
+                    hash: 0,
+                    expr: a_plus_1,
+                }),
+            ),
+            (0, None),
+            (1, None),
+            (5, None),
+            (4, None),
+            (7, None),
+        ];
         assert_eq!(expected, id_array);
 
         // include aggregates
         let mut id_array = vec![];
-        expr_to_identifier(
+        optimizer.expr_to_identifier(
             &expr,
-            &mut HashMap::new(),
+            &mut ExprStats::new(),
             &mut id_array,
             ExprMask::NormalAndAggregates,
         )?;
 
+        let hashes = collect_hashes(&mut id_array);
+        assert_eq!(hashes.len(), 5);
+
         let expected = vec![
-            (8, "{(sum(a + Int32(1)) - avg(c)) * Int32(2)|{Int32(2)}|{sum(a + 
Int32(1)) - avg(c)|{avg(c)|{c}}|{sum(a + Int32(1))|{a + 
Int32(1)|{Int32(1)}|{a}}}}}"),
-            (6, "{sum(a + Int32(1)) - avg(c)|{avg(c)|{c}}|{sum(a + 
Int32(1))|{a + Int32(1)|{Int32(1)}|{a}}}}"),
-            (3, "{sum(a + Int32(1))|{a + Int32(1)|{Int32(1)}|{a}}}"),
-            (2, "{a + Int32(1)|{Int32(1)}|{a}}"),
-            (0, ""),
-            (1, ""),
-            (5, "{avg(c)|{c}}"),
-            (4, ""),
-            (7, "")
-        ]
-        .into_iter()
-        .map(|(number, id)| (number, id.into()))
-        .collect::<Vec<_>>();
+            (
+                8,
+                Some(Identifier {
+                    hash: 0,
+                    expr: &expr,
+                }),
+            ),
+            (
+                6,
+                Some(Identifier {
+                    hash: 0,
+                    expr: sum_a_plus_1_minus_avg_c,
+                }),
+            ),
+            (
+                3,
+                Some(Identifier {
+                    hash: 0,
+                    expr: sum_a_plus_1,
+                }),
+            ),
+            (
+                2,
+                Some(Identifier {
+                    hash: 0,
+                    expr: a_plus_1,
+                }),
+            ),
+            (0, None),
+            (1, None),
+            (
+                5,
+                Some(Identifier {
+                    hash: 0,
+                    expr: avg_c,
+                }),
+            ),
+            (4, None),
+            (7, None),
+        ];
         assert_eq!(expected, id_array);
 
         Ok(())
@@ -1367,27 +1549,35 @@ mod test {
         Ok(())
     }
 
+    fn test_identifier(hash: u64, expr: &Expr) -> Identifier {
+        Identifier { hash, expr }
+    }
+
     #[test]
     fn redundant_project_fields() {
         let table_scan = test_table_scan().unwrap();
+        let c_plus_a = col("c") + col("a");
+        let b_plus_a = col("b") + col("a");
         let common_exprs_1 = CommonExprs::from([
             (
-                "c+a".to_string(),
-                (col("c") + col("a"), format!("{CSE_PREFIX}_1")),
+                test_identifier(0, &c_plus_a),
+                (c_plus_a.clone(), format!("{CSE_PREFIX}_1")),
             ),
             (
-                "b+a".to_string(),
-                (col("b") + col("a"), format!("{CSE_PREFIX}_2")),
+                test_identifier(1, &b_plus_a),
+                (b_plus_a.clone(), format!("{CSE_PREFIX}_2")),
             ),
         ]);
+        let c_plus_a_2 = col(format!("{CSE_PREFIX}_1"));
+        let b_plus_a_2 = col(format!("{CSE_PREFIX}_2"));
         let common_exprs_2 = CommonExprs::from([
             (
-                "c+a".to_string(),
-                (col(format!("{CSE_PREFIX}_1")), format!("{CSE_PREFIX}_3")),
+                test_identifier(3, &c_plus_a_2),
+                (c_plus_a_2.clone(), format!("{CSE_PREFIX}_3")),
             ),
             (
-                "b+a".to_string(),
-                (col(format!("{CSE_PREFIX}_2")), format!("{CSE_PREFIX}_4")),
+                test_identifier(4, &b_plus_a_2),
+                (b_plus_a_2.clone(), format!("{CSE_PREFIX}_4")),
             ),
         ]);
         let project = build_common_expr_project_plan(table_scan, 
common_exprs_1).unwrap();
@@ -1408,24 +1598,28 @@ mod test {
             .unwrap()
             .build()
             .unwrap();
+        let c_plus_a = col("test1.c") + col("test1.a");
+        let b_plus_a = col("test1.b") + col("test1.a");
         let common_exprs_1 = CommonExprs::from([
             (
-                "test1.c+test1.a".to_string(),
-                (col("test1.c") + col("test1.a"), format!("{CSE_PREFIX}_1")),
+                test_identifier(0, &c_plus_a),
+                (c_plus_a.clone(), format!("{CSE_PREFIX}_1")),
             ),
             (
-                "test1.b+test1.a".to_string(),
-                (col("test1.b") + col("test1.a"), format!("{CSE_PREFIX}_2")),
+                test_identifier(1, &b_plus_a),
+                (b_plus_a.clone(), format!("{CSE_PREFIX}_2")),
             ),
         ]);
+        let c_plus_a_2 = col(format!("{CSE_PREFIX}_1"));
+        let b_plus_a_2 = col(format!("{CSE_PREFIX}_2"));
         let common_exprs_2 = CommonExprs::from([
             (
-                "test1.c+test1.a".to_string(),
-                (col(format!("{CSE_PREFIX}_1")), format!("{CSE_PREFIX}_3")),
+                test_identifier(3, &c_plus_a_2),
+                (c_plus_a_2.clone(), format!("{CSE_PREFIX}_3")),
             ),
             (
-                "test1.b+test1.a".to_string(),
-                (col(format!("{CSE_PREFIX}_2")), format!("{CSE_PREFIX}_4")),
+                test_identifier(4, &b_plus_a_2),
+                (b_plus_a_2.clone(), format!("{CSE_PREFIX}_4")),
             ),
         ]);
         let project = build_common_expr_project_plan(join, 
common_exprs_1).unwrap();
@@ -1457,7 +1651,7 @@ mod test {
             .unwrap()
             .build()
             .unwrap();
-        let rule = CommonSubexprEliminate {};
+        let rule = CommonSubexprEliminate::new();
         let optimized_plan = rule.rewrite(plan, 
&OptimizerContext::new()).unwrap();
         assert!(!optimized_plan.transformed);
         let optimized_plan = optimized_plan.data;
diff --git a/datafusion/proto-common/Cargo.toml 
b/datafusion/proto-common/Cargo.toml
index 66ce7cbd83..e5d65827cd 100644
--- a/datafusion/proto-common/Cargo.toml
+++ b/datafusion/proto-common/Cargo.toml
@@ -26,7 +26,7 @@ homepage = { workspace = true }
 repository = { workspace = true }
 license = { workspace = true }
 authors = { workspace = true }
-rust-version = "1.75"
+rust-version = "1.76"
 
 # Exclude proto files so crates.io consumers don't need protoc
 exclude = ["*.proto"]
diff --git a/datafusion/proto-common/gen/Cargo.toml 
b/datafusion/proto-common/gen/Cargo.toml
index 9f8f03de6d..54ec0e4469 100644
--- a/datafusion/proto-common/gen/Cargo.toml
+++ b/datafusion/proto-common/gen/Cargo.toml
@@ -20,7 +20,7 @@ name = "gen-common"
 description = "Code generation for proto"
 version = "0.1.0"
 edition = { workspace = true }
-rust-version = "1.75"
+rust-version = "1.76"
 authors = { workspace = true }
 homepage = { workspace = true }
 repository = { workspace = true }
diff --git a/datafusion/proto/Cargo.toml b/datafusion/proto/Cargo.toml
index aa8d0e55b6..95d9e6700a 100644
--- a/datafusion/proto/Cargo.toml
+++ b/datafusion/proto/Cargo.toml
@@ -27,7 +27,7 @@ repository = { workspace = true }
 license = { workspace = true }
 authors = { workspace = true }
 # Specify MSRV here as `cargo msrv` doesn't support workspace version
-rust-version = "1.75"
+rust-version = "1.76"
 
 # Exclude proto files so crates.io consumers don't need protoc
 exclude = ["*.proto"]
diff --git a/datafusion/proto/gen/Cargo.toml b/datafusion/proto/gen/Cargo.toml
index eabaf7ba8e..401c51c945 100644
--- a/datafusion/proto/gen/Cargo.toml
+++ b/datafusion/proto/gen/Cargo.toml
@@ -20,7 +20,7 @@ name = "gen"
 description = "Code generation for proto"
 version = "0.1.0"
 edition = { workspace = true }
-rust-version = "1.75"
+rust-version = "1.76"
 authors = { workspace = true }
 homepage = { workspace = true }
 repository = { workspace = true }
diff --git a/datafusion/substrait/Cargo.toml b/datafusion/substrait/Cargo.toml
index d934dba4cf..f3f8f6e3ab 100644
--- a/datafusion/substrait/Cargo.toml
+++ b/datafusion/substrait/Cargo.toml
@@ -26,7 +26,7 @@ repository = { workspace = true }
 license = { workspace = true }
 authors = { workspace = true }
 # Specify MSRV here as `cargo msrv` doesn't support workspace version
-rust-version = "1.75"
+rust-version = "1.76"
 
 [lints]
 workspace = true


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to