Repository: apache/datafusion
Pull request #15253: Simplify display format of `AggregateFunctionExpr`, add `Expr::sql_name`
Author: login: irenjj
Labels: name: logical-expr
description: Logical plan and expressions
name: physical-expr
description: Changes to the physical-expr crates
name: core
description: Core DataFusion crate
name: sqllogictest
description: SQL Logic Tests (.slt)
Created: 2025-03-15T16:25:09Z | Merged: 2025-03-20T14:54:38Z

## 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 #15252

## 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.  
-->

## 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.
-->

## 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)?
-->

## Are there any user-facing changes?

<!--
If there are user-facing changes then we may require documentation to be updated before approving the PR.
-->

<!--
If there are any breaking changes to public APIs, please add the `api change` label.
-->


Linked issue #123: Implement physical plan for EXISTS subquery
*Note*: migrated from original JIRA: https://issues.apache.org/jira/browse/ARROW-10819

The TPC-H queries include use of the EXISTS which is used to test for the existence of any record in a subquery. For example:

```sql
and *exists* (
    select
        *
    from
        lineitem
    where
        l_orderkey = o_orderkey
        and l_commitdate < l_receiptdate
)
```
Linked issue comments: user: login: alamb
created_at: 2021-04-26T13:23:43Z
body: Comment from Andy Grove(andygrove) @ 2020-12-31T19:42:45.132+0000:
<pre>The example given here is a correlated subquery that can be translated into a join.

Here is a random stackoverflow discussion on this for reference (I have not reviewed it)

https://stackoverflow.com/questions/1772609/procedurally-transform-subquery-into-join</pre>
author_association: CONTRIBUTOR
user: login: alamb
created_at: 2022-10-21T19:21:30Z
body: In case anyone is curious -- we support correlated versions of these queries (via a join) but if there is no correlation (not super useful) we do not

```sql
❯ create table foo as select * from (values (1), (2), (NULL)) as sql
;
0 rows in set. Query took 0.022 seconds.
3 rows in set. Query took 0.007 seconds.
❯ create table bar as select * from (values (1), (2), (NULL)) as sql;
0 rows in set. Query took 0.000 seconds.
❯ select * from foo where exists (select column1 from bar);
NotImplemented("Physical plan does not support logical expression EXISTS (<subquery>)")
❯ select * from foo where exists (select column1 from bar where foo.column1 = bar.column1);
+---------+
| column1 |
+---------+
| 2       |
| 1       |
+---------+
```
author_association: CONTRIBUTOR
user: login: logan-keede
created_at: 2025-02-09T19:53:56Z
body: 
> In case anyone is curious -- we support correlated versions of these queries (via a join) but if there is no correlation (not super useful) we do not
> 
> ❯ create table foo as select * from (values (1), (2), (NULL)) as sql
> ;
> 0 rows in set. Query took 0.022 seconds.
> 3 rows in set. Query took 0.007 seconds.
> ❯ create table bar as select * from (values (1), (2), (NULL)) as sql;
> 0 rows in set. Query took 0.000 seconds.
> ❯ select * from foo where exists (select column1 from bar);
> NotImplemented("Physical plan does not support logical expression EXISTS (<subquery>)")
> ❯ select * from foo where exists (select column1 from bar where foo.column1 = bar.column1);
> +---------+
> | column1 |
> +---------+
> | 2       |
> | 1       |
> +---------+

```sql

> explain select * from foo where exists (select column1 from bar);
+---------------+-----------------------------------------------------+
| plan_type     | plan                                                |
+---------------+-----------------------------------------------------+
| logical_plan  | LeftSemi Join:                                      |
|               |   TableScan: foo projection=[column1]               |
|               |   SubqueryAlias: __correlated_sq_1                  |
|               |     TableScan: bar projection=[]                    |
| physical_plan | NestedLoopJoinExec: join_type=RightSemi             |
|               |   DataSourceExec: partitions=1, partition_sizes=[1] |
|               |   DataSourceExec: partitions=1, partition_sizes=[1] |
|               |                                                     |
+---------------+-----------------------------------------------------+
2 row(s) fetched. 
Elapsed 0.007 seconds.

> select * from foo where exists (select column1 from bar);
+---------+
| column1 |
+---------+
| 1       |
| 2       |
| NULL    |
+---------+
3 row(s) fetched. 
Elapsed 0.006 seconds.
```


I get this on `datafusion 45.0.0`, perhaps this issue can be closed or at least updated if there is a case where we still don't support EXISTS in physical plan.

author_association: CONTRIBUTOR
user: login: alamb
created_at: 2025-02-10T15:04:31Z
body: Looks good -- thanks for checking @logan-keede  -- we can open a new issue if we find another hole
author_association: CONTRIBUTOR

Diff:
diff --git a/datafusion/core/src/physical_planner.rs b/datafusion/core/src/physical_planner.rs
index 135b32a0a8d7a..bc3c1d0ac99b2 100644
--- a/datafusion/core/src/physical_planner.rs
+++ b/datafusion/core/src/physical_planner.rs
@@ -1588,6 +1588,7 @@ type AggregateExprWithOptionalArgs = (
 pub fn create_aggregate_expr_with_name_and_maybe_filter(
     e: &Expr,
     name: Option<String>,
+    human_displan: String,
     logical_input_schema: &DFSchema,
     physical_input_schema: &Schema,
     execution_props: &ExecutionProps,
@@ -1642,6 +1643,7 @@ pub fn create_aggregate_expr_with_name_and_maybe_filter(
                         .order_by(ordering_reqs)
                         .schema(Arc::new(physical_input_schema.to_owned()))
                         .alias(name)
+                        .human_display(human_displan)
                         .with_ignore_nulls(ignore_nulls)
                         .with_distinct(*distinct)
                         .build()
@@ -1664,15 +1666,22 @@ pub fn create_aggregate_expr_and_maybe_filter(
     execution_props: &ExecutionProps,
 ) -> Result<AggregateExprWithOptionalArgs> {
     // unpack (nested) aliased logical expressions, e.g. "sum(col) as total"
-    let (name, e) = match e {
-        Expr::Alias(Alias { expr, name, .. }) => (Some(name.clone()), expr.as_ref()),
-        Expr::AggregateFunction(_) => (Some(e.schema_name().to_string()), e),
-        _ => (None, e),
+    let (name, human_display, e) = match e {
+        Expr::Alias(Alias { expr, name, .. }) => {
+            (Some(name.clone()), String::default(), expr.as_ref())
+        }
+        Expr::AggregateFunction(_) => (
+            Some(e.schema_name().to_string()),
+            e.human_display().to_string(),
+            e,
+        ),
+        _ => (None, String::default(), e),
     };
 
     create_aggregate_expr_with_name_and_maybe_filter(
         e,
         name,
+        human_display,
         logical_input_schema,
         physical_input_schema,
         execution_props,
diff --git a/datafusion/expr/src/expr.rs b/datafusion/expr/src/expr.rs
index 6f110895a40ab..9f6855b698243 100644
--- a/datafusion/expr/src/expr.rs
+++ b/datafusion/expr/src/expr.rs
@@ -64,6 +64,15 @@ use sqlparser::ast::{
 ///
 /// [`ExprFunctionExt`]: crate::expr_fn::ExprFunctionExt
 ///
+/// # Printing Expressions
+///
+/// You can print `Expr`s using the the `Debug` trait, `Display` trait, or
+/// [`Self::human_display`]. See the [examples](#examples-displaying-exprs) below.
+///
+/// If you need  SQL to pass to other systems, consider using [`Unparser`].
+///
+/// [`Unparser`]: https://docs.rs/datafusion/latest/datafusion/sql/unparser/struct.Unparser.html
+///
 /// # Schema Access
 ///
 /// See [`ExprSchemable::get_type`] to access the [`DataType`] and nullability
@@ -76,9 +85,9 @@ use sqlparser::ast::{
 /// `Expr` and [`TreeNode::transform`] can be used to rewrite an expression. See
 /// the examples below and [`TreeNode`] for more information.
 ///
-/// # Examples
+/// # Examples: Creating and Using `Expr`s
 ///
-/// ## Column references and literals
+/// ## Column References and Literals
 ///
 /// [`Expr::Column`] refer to the values of columns and are often created with
 /// the [`col`] function. For example to create an expression `c1` referring to
@@ -104,6 +113,7 @@ use sqlparser::ast::{
 /// // All literals are strongly typed in DataFusion. To make an `i64` 42:
 /// let expr = lit(42i64);
 /// assert_eq!(expr, Expr::Literal(ScalarValue::Int64(Some(42))));
+/// assert_eq!(expr, Expr::Literal(ScalarValue::Int64(Some(42))));
 /// // To make a (typed) NULL:
 /// let expr = Expr::Literal(ScalarValue::Int64(None));
 /// // to make an (untyped) NULL (the optimizer will coerce this to the correct type):
@@ -171,7 +181,51 @@ use sqlparser::ast::{
 /// ]);
 /// ```
 ///
-/// # Visiting and Rewriting `Expr`s
+/// # Examples: Displaying `Exprs`
+///
+/// There are three ways to print an `Expr` depending on the usecase.
+///
+/// ## Use `Debug` trait
+///
+/// Following Rust conventions, the `Debug` implementation prints out the
+/// internal structure of the expression, which is useful for debugging.
+///
+/// ```
+/// # use datafusion_expr::{lit, col};
+/// let expr = col("c1") + lit(42);
+/// assert_eq!(format!("{expr:?}"), "BinaryExpr(BinaryExpr { left: Column(Column { relation: None, name: \"c1\" }), op: Plus, right: Literal(Int32(42)) })");
+/// ```
+///
+/// ## Use the `Display` trait  (detailed expression)
+///
+/// The `Display` implementation prints out the expression in a SQL-like form,
+/// but has additional details such as the data type of literals. This is useful
+/// for understanding the expression in more detail and is used for the low level
+/// [`ExplainFormat::Indent`] explain plan format.
+///
+/// [`ExplainFormat::Indent`]: crate::logical_plan::ExplainFormat::Indent
+///
+/// ```
+/// # use datafusion_expr::{lit, col};
+/// let expr = col("c1") + lit(42);
+/// assert_eq!(format!("{expr}"), "c1 + Int32(42)");
+/// ```
+///
+/// ## Use [`Self::human_display`] (human readable)
+///
+/// [`Self::human_display`]  prints out the expression in a SQL-like form, optimized
+/// for human consumption by end users. It is used for the
+/// [`ExplainFormat::Tree`] explain plan format.
+///
+/// [`ExplainFormat::Tree`]: crate::logical_plan::ExplainFormat::Tree
+///
+///```
+/// # use datafusion_expr::{lit, col};
+/// let expr = col("c1") + lit(42);
+/// assert_eq!(format!("{}", expr.human_display()), "c1 + 42");
+/// ```
+///
+/// # Examples: Visiting and Rewriting `Expr`s
 ///
 /// Here is an example that finds all literals in an `Expr` tree:
 /// ```
@@ -1147,6 +1201,31 @@ impl Expr {
         SchemaDisplay(self)
     }
 
+    /// Human readable display formatting for this expression.
+    ///
+    /// This function is primarily used in printing the explain tree output,
+    /// (e.g. `EXPLAIN FORMAT TREE <query>`), providing a readable format to
+    /// show how expressions are used in physical and logical plans. See the
+    /// [`Expr`] for other ways to format expressions
+    ///
+    /// Note this format is intended for human consumption rather than SQL for
+    /// other systems. If you need  SQL to pass to other systems, consider using
+    /// [`Unparser`].
+    ///
+    /// [`Unparser`]: https://docs.rs/datafusion/latest/datafusion/sql/unparser/struct.Unparser.html
+    ///
+    /// # Example
+    /// ```
+    /// # use datafusion_expr::{col, lit};
+    /// let expr = col("foo") + lit(42);
+    /// // For EXPLAIN output:
+    /// // "foo + 42"
+    /// println!("{}", expr.human_display());
+    /// ```
+    pub fn human_display(&self) -> impl Display + '_ {
+        SqlDisplay(self)
+    }
+
     /// Returns the qualifier and the schema name of this expression.
     ///
     /// Used when the expression forms the output field of a certain plan.
@@ -2596,6 +2675,187 @@ impl Display for SchemaDisplay<'_> {
     }
 }
 
+/// A helper struct for displaying an `Expr` as an SQL-like string.
+struct SqlDisplay<'a>(&'a Expr);
+
+impl Display for SqlDisplay<'_> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self.0 {
+            Expr::Literal(scalar) => scalar.fmt(f),
+            Expr::Alias(Alias { name, .. }) => write!(f, "{name}"),
+            Expr::Between(Between {
+                expr,
+                negated,
+                low,
+                high,
+            }) => {
+                if *negated {
+                    write!(
+                        f,
+                        "{} NOT BETWEEN {} AND {}",
+                        SqlDisplay(expr),
+                        SqlDisplay(low),
+                        SqlDisplay(high),
+                    )
+                } else {
+                    write!(
+                        f,
+                        "{} BETWEEN {} AND {}",
+                        SqlDisplay(expr),
+                        SqlDisplay(low),
+                        SqlDisplay(high),
+                    )
+                }
+            }
+            Expr::BinaryExpr(BinaryExpr { left, op, right }) => {
+                write!(f, "{} {op} {}", SqlDisplay(left), SqlDisplay(right),)
+            }
+            Expr::Case(Case {
+                expr,
+                when_then_expr,
+                else_expr,
+            }) => {
+                write!(f, "CASE ")?;
+
+                if let Some(e) = expr {
+                    write!(f, "{} ", SqlDisplay(e))?;
+                }
+
+                for (when, then) in when_then_expr {
+                    write!(f, "WHEN {} THEN {} ", SqlDisplay(when), SqlDisplay(then),)?;
+                }
+
+                if let Some(e) = else_expr {
+                    write!(f, "ELSE {} ", SqlDisplay(e))?;
+                }
+
+                write!(f, "END")
+            }
+            Expr::Cast(Cast { expr, .. }) | Expr::TryCast(TryCast { expr, .. }) => {
+                write!(f, "{}", SqlDisplay(expr))
+            }
+            Expr::InList(InList {
+                expr,
+                list,
+                negated,
+            }) => {
+                write!(
+                    f,
+                    "{}{} IN {}",
+                    SqlDisplay(expr),
+                    if *negated { " NOT" } else { "" },
+                    ExprListDisplay::comma_separated(list.as_slice())
+                )
+            }
+            Expr::GroupingSet(GroupingSet::Cube(exprs)) => {
+                write!(
+                    f,
+                    "ROLLUP ({})",
+                    ExprListDisplay::comma_separated(exprs.as_slice())
+                )
+            }
+            Expr::GroupingSet(GroupingSet::GroupingSets(lists_of_exprs)) => {
+                write!(f, "GROUPING SETS (")?;
+                for exprs in lists_of_exprs.iter() {
+                    write!(
+                        f,
+                        "({})",
+                        ExprListDisplay::comma_separated(exprs.as_slice())
+                    )?;
+                }
+                write!(f, ")")
+            }
+            Expr::GroupingSet(GroupingSet::Rollup(exprs)) => {
+                write!(
+                    f,
+                    "ROLLUP ({})",
+                    ExprListDisplay::comma_separated(exprs.as_slice())
+                )
+            }
+            Expr::IsNull(expr) => write!(f, "{} IS NULL", SqlDisplay(expr)),
+            Expr::IsNotNull(expr) => {
+                write!(f, "{} IS NOT NULL", SqlDisplay(expr))
+            }
+            Expr::IsUnknown(expr) => {
+                write!(f, "{} IS UNKNOWN", SqlDisplay(expr))
+            }
+            Expr::IsNotUnknown(expr) => {
+                write!(f, "{} IS NOT UNKNOWN", SqlDisplay(expr))
+            }
+            Expr::IsTrue(expr) => write!(f, "{} IS TRUE", SqlDisplay(expr)),
+            Expr::IsFalse(expr) => write!(f, "{} IS FALSE", SqlDisplay(expr)),
+            Expr::IsNotTrue(expr) => {
+                write!(f, "{} IS NOT TRUE", SqlDisplay(expr))
+            }
+            Expr::IsNotFalse(expr) => {
+                write!(f, "{} IS NOT FALSE", SqlDisplay(expr))
+            }
+            Expr::Like(Like {
+                negated,
+                expr,
+                pattern,
+                escape_char,
+                case_insensitive,
+            }) => {
+                write!(
+                    f,
+                    "{} {}{} {}",
+                    SqlDisplay(expr),
+                    if *negated { "NOT " } else { "" },
+                    if *case_insensitive { "ILIKE" } else { "LIKE" },
+                    SqlDisplay(pattern),
+                )?;
+
+                if let Some(char) = escape_char {
+                    write!(f, " CHAR '{char}'")?;
+                }
+
+                Ok(())
+            }
+            Expr::Negative(expr) => write!(f, "(- {})", SqlDisplay(expr)),
+            Expr::Not(expr) => write!(f, "NOT {}", SqlDisplay(expr)),
+            Expr::Unnest(Unnest { expr }) => {
+                write!(f, "UNNEST({})", SqlDisplay(expr))
+            }
+            Expr::SimilarTo(Like {
+                negated,
+                expr,
+                pattern,
+                escape_char,
+                ..
+            }) => {
+                write!(
+                    f,
+                    "{} {} {}",
+                    SqlDisplay(expr),
+                    if *negated {
+                        "NOT SIMILAR TO"
+                    } else {
+                        "SIMILAR TO"
+                    },
+                    SqlDisplay(pattern),
+                )?;
+                if let Some(char) = escape_char {
+                    write!(f, " CHAR '{char}'")?;
+                }
+
+                Ok(())
+            }
+            Expr::AggregateFunction(AggregateFunction { func, params }) => {
+                match func.human_display(params) {
+                    Ok(name) => {
+                        write!(f, "{name}")
+                    }
+                    Err(e) => {
+                        write!(f, "got error from schema_name {}", e)
+                    }
+                }
+            }
+            _ => write!(f, "{}", self.0),
+        }
+    }
+}
+
 /// Get schema_name for Vector of expressions
 ///
 /// Internal usage. Please call `schema_name_from_exprs` instead
@@ -2607,6 +2867,38 @@ pub(crate) fn schema_name_from_exprs_comma_separated_without_space(
     schema_name_from_exprs_inner(exprs, ",")
 }
 
+/// Formats a list of `&Expr` with a custom separator using SQL display format
+pub struct ExprListDisplay<'a> {
+    exprs: &'a [Expr],
+    sep: &'a str,
+}
+
+impl<'a> ExprListDisplay<'a> {
+    /// Create a new display struct with the given expressions and separator
+    pub fn new(exprs: &'a [Expr], sep: &'a str) -> Self {
+        Self { exprs, sep }
+    }
+
+    /// Create a new display struct with comma-space separator
+    pub fn comma_separated(exprs: &'a [Expr]) -> Self {
+        Self::new(exprs, ", ")
+    }
+}
+
+impl Display for ExprListDisplay<'_> {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        let mut first = true;
+        for expr in self.exprs {
+            if !first {
+                write!(f, "{}", self.sep)?;
+            }
+            write!(f, "{}", SqlDisplay(expr))?;
+            first = false;
+        }
+        Ok(())
+    }
+}
+
 /// Get schema_name for Vector of expressions
 pub fn schema_name_from_exprs(exprs: &[Expr]) -> Result<String, fmt::Error> {
     schema_name_from_exprs_inner(exprs, ", ")
diff --git a/datafusion/expr/src/udaf.rs b/datafusion/expr/src/udaf.rs
index f9039cea2edc7..b75e8fd3cd3c4 100644
--- a/datafusion/expr/src/udaf.rs
+++ b/datafusion/expr/src/udaf.rs
@@ -31,7 +31,7 @@ use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
 
 use crate::expr::{
     schema_name_from_exprs, schema_name_from_exprs_comma_separated_without_space,
-    schema_name_from_sorts, AggregateFunction, AggregateFunctionParams,
+    schema_name_from_sorts, AggregateFunction, AggregateFunctionParams, ExprListDisplay,
     WindowFunctionParams,
 };
 use crate::function::{
@@ -175,6 +175,13 @@ impl AggregateUDF {
         self.inner.schema_name(params)
     }
 
+    /// Returns a human readable expression.
+    ///
+    /// See [`Expr::human_display`] for details.
+    pub fn human_display(&self, params: &AggregateFunctionParams) -> Result<String> {
+        self.inner.human_display(params)
+    }
+
     pub fn window_function_schema_name(
         &self,
         params: &WindowFunctionParams,
@@ -452,6 +459,45 @@ pub trait AggregateUDFImpl: Debug + Send + Sync {
         Ok(schema_name)
     }
 
+    /// Returns a human readable expression.
+    ///
+    /// See [`Expr::human_display`] for details.
+    fn human_display(&self, params: &AggregateFunctionParams) -> Result<String> {
+        let AggregateFunctionParams {
+            args,
+            distinct,
+            filter,
+            order_by,
+            null_treatment,
+        } = params;
+
+        let mut schema_name = String::new();
+
+        schema_name.write_fmt(format_args!(
+            "{}({}{})",
+            self.name(),
+            if *distinct { "DISTINCT " } else { "" },
+            ExprListDisplay::comma_separated(args.as_slice())
+        ))?;
+
+        if let Some(null_treatment) = null_treatment {
+            schema_name.write_fmt(format_args!(" {}", null_treatment))?;
+        }
+
+        if let Some(filter) = filter {
+            schema_name.write_fmt(format_args!(" FILTER (WHERE {filter})"))?;
+        };
+
+        if let Some(order_by) = order_by {
+            schema_name.write_fmt(format_args!(
+                " ORDER BY [{}]",
+                schema_name_from_sorts(order_by)?
+            ))?;
+        };
+
+        Ok(schema_name)
+    }
+
     /// Returns the name of the column this expression would create
     ///
     /// See [`Expr::schema_name`] for details
diff --git a/datafusion/physical-expr/src/aggregate.rs b/datafusion/physical-expr/src/aggregate.rs
index 34c4e52d517e7..ae3d9050fa628 100644
--- a/datafusion/physical-expr/src/aggregate.rs
+++ b/datafusion/physical-expr/src/aggregate.rs
@@ -65,6 +65,8 @@ pub struct AggregateExprBuilder {
     /// Physical expressions of the aggregate function
     args: Vec<Arc<dyn PhysicalExpr>>,
     alias: Option<String>,
+    /// A human readable name
+    human_display: String,
     /// Arrow Schema for the aggregate function
     schema: SchemaRef,
     /// The physical order by expressions
@@ -83,6 +85,7 @@ impl AggregateExprBuilder {
             fun,
             args,
             alias: None,
+            human_display: String::default(),
             schema: Arc::new(Schema::empty()),
             ordering_req: LexOrdering::default(),
             ignore_nulls: false,
@@ -99,6 +102,7 @@ impl AggregateExprBuilder {
             fun,
             args,
             alias,
+            human_display,
             schema,
             ordering_req,
             ignore_nulls,
@@ -148,6 +152,7 @@ impl AggregateExprBuilder {
             args,
             data_type,
             name,
+            human_display,
             schema: Arc::unwrap_or_clone(schema),
             ordering_req,
             ignore_nulls,
@@ -164,6 +169,11 @@ impl AggregateExprBuilder {
         self
     }
 
+    pub fn human_display(mut self, name: String) -> Self {
+        self.human_display = name;
+        self
+    }
+
     pub fn schema(mut self, schema: SchemaRef) -> Self {
         self.schema = schema;
         self
@@ -214,7 +224,10 @@ pub struct AggregateFunctionExpr {
     args: Vec<Arc<dyn PhysicalExpr>>,
     /// Output / return type of this aggregate
     data_type: DataType,
+    /// Output column name that this expression creates
     name: String,
+    /// Simplified name for `tree` explain.
+    human_display: String,
     schema: Schema,
     // The physical order by expressions
     ordering_req: LexOrdering,
@@ -245,6 +258,11 @@ impl AggregateFunctionExpr {
         &self.name
     }
 
+    /// Simplified name for `tree` explain.
+    pub fn human_display(&self) -> &str {
+        &self.human_display
+    }
+
     /// Return if the aggregation is distinct
     pub fn is_distinct(&self) -> bool {
         self.is_distinct
diff --git a/datafusion/physical-plan/src/aggregates/mod.rs b/datafusion/physical-plan/src/aggregates/mod.rs
index 4230eeeed0c9a..84ba0fe3b630b 100644
--- a/datafusion/physical-plan/src/aggregates/mod.rs
+++ b/datafusion/physical-plan/src/aggregates/mod.rs
@@ -48,6 +48,7 @@ use datafusion_physical_expr::{
     PhysicalSortRequirement,
 };
 
+use datafusion_physical_expr_common::physical_expr::fmt_sql;
 use itertools::Itertools;
 
 pub(crate) mod group_values;
@@ -742,17 +743,18 @@ impl DisplayAs for AggregateExec {
         t: DisplayFormatType,
         f: &mut std::fmt::Formatter,
     ) -> std::fmt::Result {
-        let format_expr_with_alias =
-            |(e, alias): &(Arc<dyn PhysicalExpr>, String)| -> String {
-                let e = e.to_string();
-                if &e != alias {
-                    format!("{e} as {alias}")
-                } else {
-                    e
-                }
-            };
         match t {
             DisplayFormatType::Default | DisplayFormatType::Verbose => {
+                let format_expr_with_alias =
+                    |(e, alias): &(Arc<dyn PhysicalExpr>, String)| -> String {
+                        let e = e.to_string();
+                        if &e != alias {
+                            format!("{e} as {alias}")
+                        } else {
+                            e
+                        }
+                    };
+
                 write!(f, "AggregateExec: mode={:?}", self.mode)?;
                 let g: Vec<String> = if self.group_by.is_single() {
                     self.group_by
@@ -801,6 +803,16 @@ impl DisplayAs for AggregateExec {
                 }
             }
             DisplayFormatType::TreeRender => {
+                let format_expr_with_alias =
+                    |(e, alias): &(Arc<dyn PhysicalExpr>, String)| -> String {
+                        let expr_sql = fmt_sql(e.as_ref()).to_string();
+                        if &expr_sql != alias {
+                            format!("{expr_sql} as {alias}")
+                        } else {
+                            expr_sql
+                        }
+                    };
+
                 let g: Vec<String> = if self.group_by.is_single() {
                     self.group_by
                         .expr
@@ -830,17 +842,18 @@ impl DisplayAs for AggregateExec {
                         })
                         .collect()
                 };
-                // TODO: Implement `fmt_sql` for `AggregateFunctionExpr`.
                 let a: Vec<String> = self
                     .aggr_expr
                     .iter()
-                    .map(|agg| agg.name().to_string())
+                    .map(|agg| agg.human_display().to_string())
                     .collect();
                 writeln!(f, "mode={:?}", self.mode)?;
                 if !g.is_empty() {
                     writeln!(f, "group_by={}", g.join(", "))?;
                 }
-                writeln!(f, "aggr={}", a.join(", "))?;
+                if !a.is_empty() {
+                    writeln!(f, "aggr={}", a.join(", "))?;
+                }
             }
         }
         Ok(())
diff --git a/datafusion/sqllogictest/test_files/explain_tree.slt b/datafusion/sqllogictest/test_files/explain_tree.slt
index 22b1b46019752..aaea05be76b54 100644
--- a/datafusion/sqllogictest/test_files/explain_tree.slt
+++ b/datafusion/sqllogictest/test_files/explain_tree.slt
@@ -204,53 +204,50 @@ physical_plan
 04)│           aggr:           │
 05)│   sum(table1.bigint_col)  │
 06)│                           │
-07)│         group_by:         │
-08)│ string_col@0 as string_col│
-09)│                           │
-10)│           mode:           │
-11)│      FinalPartitioned     │
-12)└─────────────┬─────────────┘
-13)┌─────────────┴─────────────┐
-14)│    CoalesceBatchesExec    │
-15)│    --------------------   │
-16)│     target_batch_size:    │
-17)│            8192           │
-18)└─────────────┬─────────────┘
-19)┌─────────────┴─────────────┐
-20)│      RepartitionExec      │
-21)│    --------------------   │
-22)│  output_partition_count:  │
-23)│             4             │
-24)│                           │
-25)│    partitioning_scheme:   │
-26)│  Hash([string_col@0], 4)  │
-27)└─────────────┬─────────────┘
-28)┌─────────────┴─────────────┐
-29)│       AggregateExec       │
-30)│    --------------------   │
-31)│           aggr:           │
-32)│   sum(table1.bigint_col)  │
-33)│                           │
-34)│         group_by:         │
-35)│ string_col@0 as string_col│
-36)│                           │
-37)│       mode: Partial       │
-38)└─────────────┬─────────────┘
-39)┌─────────────┴─────────────┐
-40)│      RepartitionExec      │
-41)│    --------------------   │
-42)│  output_partition_count:  │
-43)│             1             │
-44)│                           │
-45)│    partitioning_scheme:   │
-46)│     RoundRobinBatch(4)    │
-47)└─────────────┬─────────────┘
-48)┌─────────────┴─────────────┐
-49)│       DataSourceExec      │
-50)│    --------------------   │
-51)│          files: 1         │
-52)│        format: csv        │
-53)└───────────────────────────┘
+07)│    group_by: string_col   │
+08)│                           │
+09)│           mode:           │
+10)│      FinalPartitioned     │
+11)└─────────────┬─────────────┘
+12)┌─────────────┴─────────────┐
+13)│    CoalesceBatchesExec    │
+14)│    --------------------   │
+15)│     target_batch_size:    │
+16)│            8192           │
+17)└─────────────┬─────────────┘
+18)┌─────────────┴─────────────┐
+19)│      RepartitionExec      │
+20)│    --------------------   │
+21)│  output_partition_count:  │
+22)│             4             │
+23)│                           │
+24)│    partitioning_scheme:   │
+25)│  Hash([string_col@0], 4)  │
+26)└─────────────┬─────────────┘
+27)┌─────────────┴─────────────┐
+28)│       AggregateExec       │
+29)│    --------------------   │
+30)│           aggr:           │
+31)│   sum(table1.bigint_col)  │
+32)│                           │
+33)│    group_by: string_col   │
+34)│       mode: Partial       │
+35)└─────────────┬─────────────┘
+36)┌─────────────┴─────────────┐
+37)│      RepartitionExec      │
+38)│    --------------------   │
+39)│  output_partition_count:  │
+40)│             1             │
+41)│                           │
+42)│    partitioning_scheme:   │
+43)│     RoundRobinBatch(4)    │
+44)└─────────────┬─────────────┘
+45)┌─────────────┴─────────────┐
+46)│       DataSourceExec      │
+47)│    --------------------   │
+48)│          files: 1         │
+49)│        format: csv        │
+50)└───────────────────────────┘
 
 
 # Limit
@@ -1339,7 +1336,7 @@ physical_plan
 12)-----------------------------┌─────────────┴─────────────┐
 13)-----------------------------│       AggregateExec       │
 14)-----------------------------│    --------------------   │
-15)-----------------------------│   aggr: count(Int64(1))   │
+15)-----------------------------│       aggr: count(1)      │
 16)-----------------------------│        mode: Final        │
 17)-----------------------------└─────────────┬─────────────┘
 18)-----------------------------┌─────────────┴─────────────┐
@@ -1348,7 +1345,7 @@ physical_plan
 21)-----------------------------┌─────────────┴─────────────┐
 22)-----------------------------│       AggregateExec       │
 23)-----------------------------│    --------------------   │
-24)-----------------------------│   aggr: count(Int64(1))   │
+24)-----------------------------│       aggr: count(1)      │
 25)-----------------------------│       mode: Partial       │
 26)-----------------------------└─────────────┬─────────────┘
 27)-----------------------------┌─────────────┴─────────────┐
@@ -1477,69 +1474,60 @@ physical_plan
 07)┌─────────────┴─────────────┐
 08)│       AggregateExec       │
 09)│    --------------------   │
-10)│   aggr: count(Int64(1))   │
-11)│                           │
-12)│         group_by:         │
-13)│       name@0 as name      │
-14)│                           │
-15)│           mode:           │
-16)│     SinglePartitioned     │
-17)└─────────────┬─────────────┘
-18)┌─────────────┴─────────────┐
-19)│       InterleaveExec      ├──────────────┐
-20)└─────────────┬─────────────┘              │
-21)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-22)│       AggregateExec       ││       AggregateExec       │
-23)│    --------------------   ││    --------------------   │
-24)│            aggr           ││            aggr           │
-25)│                           ││                           │
-26)│         group_by:         ││         group_by:         │
-27)│       name@0 as name      ││       name@0 as name      │
-28)│                           ││                           │
-29)│           mode:           ││           mode:           │
-30)│      FinalPartitioned     ││      FinalPartitioned     │
-31)└─────────────┬─────────────┘└─────────────┬─────────────┘
-32)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-33)│    CoalesceBatchesExec    ││    CoalesceBatchesExec    │
-34)│    --------------------   ││    --------------------   │
-35)│     target_batch_size:    ││     target_batch_size:    │
-36)│            8192           ││            8192           │
-37)└─────────────┬─────────────┘└─────────────┬─────────────┘
-38)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-39)│      RepartitionExec      ││      RepartitionExec      │
-40)│    --------------------   ││    --------------------   │
-41)│  output_partition_count:  ││  output_partition_count:  │
-42)│             4             ││             4             │
-43)│                           ││                           │
-44)│    partitioning_scheme:   ││    partitioning_scheme:   │
-45)│     Hash([name@0], 4)     ││     Hash([name@0], 4)     │
-46)└─────────────┬─────────────┘└─────────────┬─────────────┘
-47)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-48)│      RepartitionExec      ││      RepartitionExec      │
-49)│    --------------------   ││    --------------------   │
-50)│  output_partition_count:  ││  output_partition_count:  │
-51)│             1             ││             1             │
-52)│                           ││                           │
-53)│    partitioning_scheme:   ││    partitioning_scheme:   │
-54)│     RoundRobinBatch(4)    ││     RoundRobinBatch(4)    │
-55)└─────────────┬─────────────┘└─────────────┬─────────────┘
-56)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-57)│       AggregateExec       ││       AggregateExec       │
-58)│    --------------------   ││    --------------------   │
-59)│            aggr           ││            aggr           │
-60)│                           ││                           │
-61)│         group_by:         ││         group_by:         │
-62)│       name@0 as name      ││       name@0 as name      │
-63)│                           ││                           │
-64)│       mode: Partial       ││       mode: Partial       │
-65)└─────────────┬─────────────┘└─────────────┬─────────────┘
-66)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-67)│       DataSourceExec      ││       DataSourceExec      │
-68)│    --------------------   ││    --------------------   │
-69)│        bytes: 1320        ││        bytes: 1312        │
-70)│       format: memory      ││       format: memory      │
-71)│          rows: 1          ││          rows: 1          │
-72)└───────────────────────────┘└───────────────────────────┘
+10)│       aggr: count(1)      │
+11)│       group_by: name      │
+12)│                           │
+13)│           mode:           │
+14)│     SinglePartitioned     │
+15)└─────────────┬─────────────┘
+16)┌─────────────┴─────────────┐
+17)│       InterleaveExec      ├──────────────┐
+18)└─────────────┬─────────────┘              │
+19)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
+20)│       AggregateExec       ││       AggregateExec       │
+21)│    --------------------   ││    --------------------   │
+22)│       group_by: name      ││       group_by: name      │
+23)│                           ││                           │
+24)│           mode:           ││           mode:           │
+25)│      FinalPartitioned     ││      FinalPartitioned     │
+26)└─────────────┬─────────────┘└─────────────┬─────────────┘
+27)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
+28)│    CoalesceBatchesExec    ││    CoalesceBatchesExec    │
+29)│    --------------------   ││    --------------------   │
+30)│     target_batch_size:    ││     target_batch_size:    │
+31)│            8192           ││            8192           │
+32)└─────────────┬─────────────┘└─────────────┬─────────────┘
+33)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
+34)│      RepartitionExec      ││      RepartitionExec      │
+35)│    --------------------   ││    --------------------   │
+36)│  output_partition_count:  ││  output_partition_count:  │
+37)│             4             ││             4             │
+38)│                           ││                           │
+39)│    partitioning_scheme:   ││    partitioning_scheme:   │
+40)│     Hash([name@0], 4)     ││     Hash([name@0], 4)     │
+41)└─────────────┬─────────────┘└─────────────┬─────────────┘
+42)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
+43)│      RepartitionExec      ││      RepartitionExec      │
+44)│    --------------------   ││    --------------------   │
+45)│  output_partition_count:  ││  output_partition_count:  │
+46)│             1             ││             1             │
+47)│                           ││                           │
+48)│    partitioning_scheme:   ││    partitioning_scheme:   │
+49)│     RoundRobinBatch(4)    ││     RoundRobinBatch(4)    │
+50)└─────────────┬─────────────┘└─────────────┬─────────────┘
+51)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
+52)│       AggregateExec       ││       AggregateExec       │
+53)│    --------------------   ││    --------------------   │
+54)│       group_by: name      ││       group_by: name      │
+55)│       mode: Partial       ││       mode: Partial       │
+56)└─────────────┬─────────────┘└─────────────┬─────────────┘
+57)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
+58)│       DataSourceExec      ││       DataSourceExec      │
+59)│    --------------------   ││    --------------------   │
+60)│        bytes: 1320        ││        bytes: 1312        │
+61)│       format: memory      ││       format: memory      │
+62)│          rows: 1          ││          rows: 1          │
+63)└───────────────────────────┘└───────────────────────────┘
 
 # Test explain tree for UnionExec
 query TT
@@ -1975,7 +1963,7 @@ physical_plan
 07)┌─────────────┴─────────────┐
 08)│       AggregateExec       │
 09)│    --------------------   │
-10)│   aggr: count(Int64(1))   │
+10)│       aggr: count(1)      │
 11)│        mode: Final        │
 12)└─────────────┬─────────────┘
 13)┌─────────────┴─────────────┐
@@ -1984,7 +1972,7 @@ physical_plan
 16)┌─────────────┴─────────────┐
 17)│       AggregateExec       │
 18)│    --------------------   │
-19)│   aggr: count(Int64(1))   │
+19)│       aggr: count(1)      │
 20)│       mode: Partial       │
 21)└─────────────┬─────────────┘
 22)┌─────────────┴─────────────┐


Reviews:
user: login: irenjj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T16:35:41Z
user: login: alamb
body: Thank you @irenjj  -- this looks really nice

I think there are a few ways we should improve the comments, ~but I'll directly push those to this branch.~

Thanks again!

Update; I don't have time right now to make the comment changes. Let me know what you think @irenjj  and if you are willing to make them in this PR. If not I'll make a follow on PR tomorrow.

Thanks again ❤️ 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T18:42:06Z
user: login: jayzhan211
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T00:36:51Z
user: login: jayzhan211
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T00:38:46Z
user: login: jayzhan211
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T00:40:16Z
user: login: irenjj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T04:34:11Z
user: login: xudong963
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T11:28:46Z
user: login: irenjj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T11:52:58Z
user: login: xudong963
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T13:54:16Z
user: login: irenjj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:12:21Z
user: login: irenjj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:22:08Z
user: login: alamb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T19:25:31Z
user: login: alamb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T19:28:54Z
user: login: jayzhan211
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T00:24:05Z
user: login: xudong963
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T14:00:13Z

Review comments:
diff_hunk: @@ -2596,6 +2600,43 @@ impl Display for SchemaDisplay<'_> {
     }
 }
 
+struct SqlDisplay<'a>(&'a Expr);
+impl Display for SqlDisplay<'_> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self.0 {
+            Expr::Column(c) => {
+                write!(f, "{}", c.name)
+            }
+            Expr::Literal(_) => {
+                write!(f, "aa")
+            }
+            Expr::ScalarVariable(..) => {
+                write!(f, "bb")
+            }
+            Expr::OuterReferenceColumn(..) => {
+                write!(f, "cc")
+            }
+            Expr::Placeholder(_) => {
+                write!(f, "dd")
+            }
+            Expr::Wildcard { .. } => {
+                write!(f, "ee")
+            }
+            Expr::AggregateFunction(AggregateFunction { func, params }) => {
+                match func.sql_name(params) {
+                    Ok(name) => {
+                        write!(f, "{name}")
+                    }
+                    Err(e) => {
+                        write!(f, "got error from schema_name {}", e)
+                    }
+                }
+            }
+            _ => write!(f, "{}", self.0.schema_name()),
+        }
+    }
+}
+
path: datafusion/expr/src/expr.rs
user: login: irenjj
body: Just a demo here.
`AggregateFunctionExpr`'s `name` is constructed at logical expr phase, we need a new member `sql_name` for tree explain, and a new constructor `SqlDisplay` to handle the inner `Expr` recursively. cc @alamb 👀
created_at: 2025-03-15T16:34:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1338,7 +1336,7 @@ physical_plan
 12)-----------------------------┌─────────────┴─────────────┐
 13)-----------------------------│       AggregateExec       │
 14)-----------------------------│    --------------------   │
-15)-----------------------------│   aggr: count(Int64(1))   │
+15)-----------------------------│      aggr: count(aa)      │
path: datafusion/sqllogictest/test_files/explain_tree.slt
user: login: irenjj
body: We can see the final display changed.
created_at: 2025-03-15T16:35:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2596,6 +2612,176 @@ impl Display for SchemaDisplay<'_> {
     }
 }
 
+struct SqlDisplay<'a>(&'a Expr);
+impl Display for SqlDisplay<'_> {
path: datafusion/expr/src/expr.rs
user: login: alamb
body: We already have code to convert from `Expr` to SQL, in https://docs.rs/datafusion/latest/datafusion/sql/unparser/struct.Unparser.html#method.expr_to_sql

However, datafusion-expr doesn't currently depend on the datafusion-sql crate which is probably a good thing

So I think we should add a note in `Expr::sql_name` that if the user's want syntactically correct SQL to feed to some other system, they should use the `Unparser` and leave a link to https://docs.rs/datafusion/latest/datafusion/sql/unparser/struct.Unparser.html
created_at: 2025-03-17T16:15:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2607,11 +2793,23 @@ pub(crate) fn schema_name_from_exprs_comma_separated_without_space(
     schema_name_from_exprs_inner(exprs, ",")
 }
 
+/// Get `sql_name` for Vector of expressions.
path: datafusion/expr/src/expr.rs
user: login: alamb
body: Instead of adding so many new so specific functions, what do you think about adding something like the following. I think the API would be easier to understand and it would also let us save a `String` copy when formatting as sql

```rust
/// Formats a list of `&Expr` with a custom separator
struct ExprListDisplay<'a> {
  exprs: &'a [Expr],
  sep: &'a str,
}
...
impl <'a> Display for  ExprListDisplay<'a> {
...
}
```

created_at: 2025-03-17T18:31:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,6 +224,7 @@ pub struct AggregateFunctionExpr {
     /// Output / return type of this aggregate
     data_type: DataType,
     name: String,
+    sql_name: String,
path: datafusion/physical-expr/src/aggregate.rs
user: login: alamb
body: I am worried about adding this new String for every single AggregateFunctionExpr even when we are not printing an explain plan -- we are trying try to keep DataFusion's planning time from getting out of hand so if we can avoid forcing a new String  that would be better
created_at: 2025-03-17T18:34:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,6 +224,7 @@ pub struct AggregateFunctionExpr {
     /// Output / return type of this aggregate
     data_type: DataType,
     name: String,
+    sql_name: String,
path: datafusion/physical-expr/src/aggregate.rs
user: login: alamb
body: I played around with it and I couldn't find any way to avoid this. 

```suggestion
    /// Simplified name for `tree` explain.
        sql_name: String,
```
created_at: 2025-03-17T18:38:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,6 +224,7 @@ pub struct AggregateFunctionExpr {
     /// Output / return type of this aggregate
     data_type: DataType,
     name: String,
path: datafusion/physical-expr/src/aggregate.rs
user: login: alamb
body: ```suggestion
    /// Output column name that this expression creates
    name: String,
```
created_at: 2025-03-17T18:38:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -801,6 +803,16 @@ impl DisplayAs for AggregateExec {
                 }
             }
             DisplayFormatType::TreeRender => {
+                let format_expr_with_alias =
+                    |(e, alias): &(Arc<dyn PhysicalExpr>, String)| -> String {
+                        let expr_sql = fmt_sql(e.as_ref()).to_string();
path: datafusion/physical-plan/src/aggregates/mod.rs
user: login: alamb
body: I think you can avoid a string copy here like:
```suggestion
                        let expr_sql = fmt_sql(e.as_ref());
```

And then directly wrote them to `f` rather than creating strings

However I don't think this is performance critical code so it is also ok this way too (and it follows the previous pattern)

Maybe as a follow on PR we can avoid as many copies in this code
created_at: 2025-03-17T18:40:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -204,53 +204,50 @@ physical_plan
 04)│           aggr:           │
 05)│   sum(table1.bigint_col)  │
 06)│                           │
-07)│         group_by:         │
-08)│ string_col@0 as string_col│
-09)│                           │
-10)│           mode:           │
-11)│      FinalPartitioned     │
-12)└─────────────┬─────────────┘
-13)┌─────────────┴─────────────┐
-14)│    CoalesceBatchesExec    │
-15)│    --------------------   │
-16)│     target_batch_size:    │
-17)│            8192           │
-18)└─────────────┬─────────────┘
-19)┌─────────────┴─────────────┐
-20)│      RepartitionExec      │
-21)│    --------------------   │
-22)│  output_partition_count:  │
-23)│             4             │
-24)│                           │
-25)│    partitioning_scheme:   │
-26)│  Hash([string_col@0], 4)  │
-27)└─────────────┬─────────────┘
-28)┌─────────────┴─────────────┐
-29)│       AggregateExec       │
-30)│    --------------------   │
-31)│           aggr:           │
-32)│   sum(table1.bigint_col)  │
-33)│                           │
-34)│         group_by:         │
-35)│ string_col@0 as string_col│
-36)│                           │
-37)│       mode: Partial       │
-38)└─────────────┬─────────────┘
-39)┌─────────────┴─────────────┐
-40)│      RepartitionExec      │
-41)│    --------------------   │
-42)│  output_partition_count:  │
-43)│             1             │
-44)│                           │
-45)│    partitioning_scheme:   │
-46)│     RoundRobinBatch(4)    │
-47)└─────────────┬─────────────┘
-48)┌─────────────┴─────────────┐
-49)│       DataSourceExec      │
-50)│    --------------------   │
-51)│          files: 1         │
-52)│        format: csv        │
-53)└───────────────────────────┘
+07)│    group_by: string_col   │
path: datafusion/sqllogictest/test_files/explain_tree.slt
user: login: alamb
body: 💯 
created_at: 2025-03-17T18:40:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1477,69 +1474,60 @@ physical_plan
 07)┌─────────────┴─────────────┐
 08)│       AggregateExec       │
 09)│    --------------------   │
-10)│   aggr: count(Int64(1))   │
-11)│                           │
-12)│         group_by:         │
-13)│       name@0 as name      │
-14)│                           │
-15)│           mode:           │
-16)│     SinglePartitioned     │
-17)└─────────────┬─────────────┘
-18)┌─────────────┴─────────────┐
-19)│       InterleaveExec      ├──────────────┐
-20)└─────────────┬─────────────┘              │
-21)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-22)│       AggregateExec       ││       AggregateExec       │
-23)│    --------------------   ││    --------------------   │
-24)│            aggr           ││            aggr           │
-25)│                           ││                           │
-26)│         group_by:         ││         group_by:         │
-27)│       name@0 as name      ││       name@0 as name      │
-28)│                           ││                           │
-29)│           mode:           ││           mode:           │
-30)│      FinalPartitioned     ││      FinalPartitioned     │
-31)└─────────────┬─────────────┘└─────────────┬─────────────┘
-32)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-33)│    CoalesceBatchesExec    ││    CoalesceBatchesExec    │
-34)│    --------------------   ││    --------------------   │
-35)│     target_batch_size:    ││     target_batch_size:    │
-36)│            8192           ││            8192           │
-37)└─────────────┬─────────────┘└─────────────┬─────────────┘
-38)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-39)│      RepartitionExec      ││      RepartitionExec      │
-40)│    --------------------   ││    --------------------   │
-41)│  output_partition_count:  ││  output_partition_count:  │
-42)│             4             ││             4             │
-43)│                           ││                           │
-44)│    partitioning_scheme:   ││    partitioning_scheme:   │
-45)│     Hash([name@0], 4)     ││     Hash([name@0], 4)     │
-46)└─────────────┬─────────────┘└─────────────┬─────────────┘
-47)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-48)│      RepartitionExec      ││      RepartitionExec      │
-49)│    --------------------   ││    --------------------   │
-50)│  output_partition_count:  ││  output_partition_count:  │
-51)│             1             ││             1             │
-52)│                           ││                           │
-53)│    partitioning_scheme:   ││    partitioning_scheme:   │
-54)│     RoundRobinBatch(4)    ││     RoundRobinBatch(4)    │
-55)└─────────────┬─────────────┘└─────────────┬─────────────┘
-56)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-57)│       AggregateExec       ││       AggregateExec       │
-58)│    --------------------   ││    --------------------   │
-59)│            aggr           ││            aggr           │
-60)│                           ││                           │
-61)│         group_by:         ││         group_by:         │
-62)│       name@0 as name      ││       name@0 as name      │
-63)│                           ││                           │
-64)│       mode: Partial       ││       mode: Partial       │
-65)└─────────────┬─────────────┘└─────────────┬─────────────┘
-66)┌─────────────┴─────────────┐┌─────────────┴─────────────┐
-67)│       DataSourceExec      ││       DataSourceExec      │
-68)│    --------------------   ││    --------------------   │
-69)│        bytes: 1320        ││        bytes: 1312        │
-70)│       format: memory      ││       format: memory      │
-71)│          rows: 1          ││          rows: 1          │
-72)└───────────────────────────┘└───────────────────────────┘
+10)│       aggr: count(1)      │
path: datafusion/sqllogictest/test_files/explain_tree.slt
user: login: alamb
body: this is wonderful
created_at: 2025-03-17T18:41:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2596,6 +2612,176 @@ impl Display for SchemaDisplay<'_> {
     }
 }
 
+struct SqlDisplay<'a>(&'a Expr);
+impl Display for SqlDisplay<'_> {
path: datafusion/expr/src/expr.rs
user: login: jayzhan211
body: What is the main difference between `Display`? I found alias is different but others looks similar
created_at: 2025-03-18T00:36:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2596,6 +2612,176 @@ impl Display for SchemaDisplay<'_> {
     }
 }
 
+struct SqlDisplay<'a>(&'a Expr);
+impl Display for SqlDisplay<'_> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self.0 {
+            Expr::Literal(scalar) => scalar.fmt(f),
+            Expr::Placeholder(holder) => {
+                write!(f, "{}", holder.id)
+            }
+            Expr::Alias(Alias { name, .. }) => write!(f, "{name}"),
+            Expr::Between(Between {
+                expr,
+                negated,
+                low,
+                high,
+            }) => {
+                if *negated {
+                    write!(
+                        f,
+                        "{} NOT BETWEEN {} AND {}",
+                        SqlDisplay(expr),
+                        SqlDisplay(low),
+                        SqlDisplay(high),
+                    )
+                } else {
+                    write!(
+                        f,
+                        "{} BETWEEN {} AND {}",
+                        SqlDisplay(expr),
+                        SqlDisplay(low),
+                        SqlDisplay(high),
+                    )
+                }
+            }
+            Expr::BinaryExpr(BinaryExpr { left, op, right }) => {
+                write!(f, "{} {op} {}", SqlDisplay(left), SqlDisplay(right),)
+            }
+            Expr::Case(Case {
+                expr,
+                when_then_expr,
+                else_expr,
+            }) => {
+                write!(f, "CASE ")?;
+
+                if let Some(e) = expr {
+                    write!(f, "{} ", SqlDisplay(e))?;
+                }
+
+                for (when, then) in when_then_expr {
+                    write!(f, "WHEN {} THEN {} ", SqlDisplay(when), SqlDisplay(then),)?;
+                }
+
+                if let Some(e) = else_expr {
+                    write!(f, "ELSE {} ", SqlDisplay(e))?;
+                }
+
+                write!(f, "END")
+            }
+            Expr::Cast(Cast { expr, .. }) | Expr::TryCast(TryCast { expr, .. }) => {
+                write!(f, "{}", SqlDisplay(expr))
+            }
+            Expr::InList(InList {
+                expr,
+                list,
+                negated,
+            }) => {
+                let inlist_name = sql_name_from_exprs(list)?;
+
+                if *negated {
+                    write!(f, "{} NOT IN {}", SqlDisplay(expr), inlist_name)
+                } else {
+                    write!(f, "{} IN {}", SqlDisplay(expr), inlist_name)
+                }
+            }
+            Expr::GroupingSet(GroupingSet::Cube(exprs)) => {
+                write!(f, "ROLLUP ({})", sql_name_from_exprs(exprs)?)
+            }
+            Expr::GroupingSet(GroupingSet::GroupingSets(lists_of_exprs)) => {
+                write!(f, "GROUPING SETS (")?;
+                for exprs in lists_of_exprs.iter() {
+                    write!(f, "({})", sql_name_from_exprs(exprs)?)?;
+                }
+                write!(f, ")")
+            }
+            Expr::GroupingSet(GroupingSet::Rollup(exprs)) => {
+                write!(f, "ROLLUP ({})", sql_name_from_exprs(exprs)?)
+            }
+            Expr::IsNull(expr) => write!(f, "{} IS NULL", SqlDisplay(expr)),
+            Expr::IsNotNull(expr) => {
+                write!(f, "{} IS NOT NULL", SqlDisplay(expr))
+            }
+            Expr::IsUnknown(expr) => {
+                write!(f, "{} IS UNKNOWN", SqlDisplay(expr))
+            }
+            Expr::IsNotUnknown(expr) => {
+                write!(f, "{} IS NOT UNKNOWN", SqlDisplay(expr))
+            }
+            Expr::IsTrue(expr) => write!(f, "{} IS TRUE", SqlDisplay(expr)),
+            Expr::IsFalse(expr) => write!(f, "{} IS FALSE", SqlDisplay(expr)),
+            Expr::IsNotTrue(expr) => {
+                write!(f, "{} IS NOT TRUE", SqlDisplay(expr))
+            }
+            Expr::IsNotFalse(expr) => {
+                write!(f, "{} IS NOT FALSE", SqlDisplay(expr))
+            }
+            Expr::Like(Like {
+                negated,
+                expr,
+                pattern,
+                escape_char,
+                case_insensitive,
+            }) => {
+                write!(
+                    f,
+                    "{} {}{} {}",
+                    SqlDisplay(expr),
+                    if *negated { "NOT " } else { "" },
+                    if *case_insensitive { "ILIKE" } else { "LIKE" },
+                    SqlDisplay(pattern),
+                )?;
+
+                if let Some(char) = escape_char {
+                    write!(f, " CHAR '{char}'")?;
+                }
+
+                Ok(())
+            }
+            Expr::Negative(expr) => write!(f, "(- {})", SqlDisplay(expr)),
+            Expr::Not(expr) => write!(f, "NOT {}", SqlDisplay(expr)),
+            Expr::Unnest(Unnest { expr }) => {
+                write!(f, "UNNEST({})", SqlDisplay(expr))
+            }
+            Expr::SimilarTo(Like {
+                negated,
+                expr,
+                pattern,
+                escape_char,
+                ..
+            }) => {
+                write!(
+                    f,
+                    "{} {} {}",
+                    SqlDisplay(expr),
+                    if *negated {
+                        "NOT SIMILAR TO"
+                    } else {
+                        "SIMILAR TO"
+                    },
+                    SqlDisplay(pattern),
+                )?;
+                if let Some(char) = escape_char {
+                    write!(f, " CHAR '{char}'")?;
+                }
+
+                Ok(())
+            }
+            Expr::AggregateFunction(AggregateFunction { func, params }) => {
+                match func.sql_name(params) {
+                    Ok(name) => {
+                        write!(f, "{name}")
+                    }
+                    Err(e) => {
+                        write!(f, "got error from schema_name {}", e)
+                    }
+                }
+            }
+            _ => write!(f, "{}", self.0.schema_name()),
path: datafusion/expr/src/expr.rs
user: login: jayzhan211
body: I think by default should be Display, `self.0.to_string()`
created_at: 2025-03-18T00:38:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2607,11 +2793,23 @@ pub(crate) fn schema_name_from_exprs_comma_separated_without_space(
     schema_name_from_exprs_inner(exprs, ",")
 }
 
+/// Get `sql_name` for Vector of expressions.
+pub(crate) fn sql_name_from_exprs_comma_separated_without_space(
path: datafusion/expr/src/expr.rs
user: login: jayzhan211
body: `without_space` is the old format and ideally we should standardize the format #10364 so better not to introduce another format without good reason
created_at: 2025-03-18T00:40:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2596,6 +2612,176 @@ impl Display for SchemaDisplay<'_> {
     }
 }
 
+struct SqlDisplay<'a>(&'a Expr);
+impl Display for SqlDisplay<'_> {
path: datafusion/expr/src/expr.rs
user: login: irenjj
body: That's true, it's redundant for most cases.
created_at: 2025-03-18T04:34:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,53 +202,48 @@ physical_plan
 02)│       AggregateExec       │
 03)│    --------------------   │
 04)│   aggr: sum(bigint_col)   │
-05)│                           │
-06)│         group_by:         │
-07)│ string_col@0 as string_col│
path: datafusion/sqllogictest/test_files/explain_tree.slt
user: login: xudong963
body: I think it'll be helpful to keep the index `@0`, what do you think?
created_at: 2025-03-18T11:28:46Z
author_association: MEMBER
diff_hunk: @@ -202,53 +202,48 @@ physical_plan
 02)│       AggregateExec       │
 03)│    --------------------   │
 04)│   aggr: sum(bigint_col)   │
-05)│                           │
-06)│         group_by:         │
-07)│ string_col@0 as string_col│
path: datafusion/sqllogictest/test_files/explain_tree.slt
user: login: irenjj
body: It's true that column indices provide precise tracking and tracing capabilities for complex query analysis, but I think tree explain should be as simple as possible🤔, as doc says:
```
    /// TreeRender, displayed in the `tree` explain type.
    ///
    /// This format is inspired by DuckDB's explain plans. The information
    /// presented should be "user friendly", and contain only the most relevant
    /// information for understanding a plan. It should NOT contain the same level
    /// of detail information as the  [`Self::Default`] format.
 ```
created_at: 2025-03-18T11:52:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,53 +202,48 @@ physical_plan
 02)│       AggregateExec       │
 03)│    --------------------   │
 04)│   aggr: sum(bigint_col)   │
-05)│                           │
-06)│         group_by:         │
-07)│ string_col@0 as string_col│
path: datafusion/sqllogictest/test_files/explain_tree.slt
user: login: xudong963
body: got it
created_at: 2025-03-18T13:54:14Z
author_association: MEMBER
diff_hunk: @@ -801,6 +803,16 @@ impl DisplayAs for AggregateExec {
                 }
             }
             DisplayFormatType::TreeRender => {
+                let format_expr_with_alias =
+                    |(e, alias): &(Arc<dyn PhysicalExpr>, String)| -> String {
+                        let expr_sql = fmt_sql(e.as_ref()).to_string();
path: datafusion/physical-plan/src/aggregates/mod.rs
user: login: irenjj
body: We have to construct a string to compare with alias.
created_at: 2025-03-18T14:12:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2596,6 +2612,176 @@ impl Display for SchemaDisplay<'_> {
     }
 }
 
+struct SqlDisplay<'a>(&'a Expr);
+impl Display for SqlDisplay<'_> {
path: datafusion/expr/src/expr.rs
user: login: irenjj
body: Maybe we should take nested expr into consideration, like `aggr(case aggr() when...)`
created_at: 2025-03-18T14:22:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,6 +64,15 @@ use sqlparser::ast::{
 ///
 /// [`ExprFunctionExt`]: crate::expr_fn::ExprFunctionExt
 ///
+/// # Printing Expressions
path: datafusion/expr/src/expr.rs
user: login: alamb
body: I added this section and the one below to try and explain the uses of the different explain variants. 
created_at: 2025-03-18T19:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,53 +202,48 @@ physical_plan
 02)│       AggregateExec       │
 03)│    --------------------   │
 04)│   aggr: sum(bigint_col)   │
-05)│                           │
-06)│         group_by:         │
-07)│ string_col@0 as string_col│
path: datafusion/sqllogictest/test_files/explain_tree.slt
user: login: alamb
body: I think the key difference `Display` and the now named `human_format` is that Display has more details -- I tried to add comments that explain this
created_at: 2025-03-19T19:28:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: irenjj
created_at: 2025-03-18T00:10:22Z
body: > Thank you @irenjj -- this looks really nice
> 
> I think there are a few ways we should improve the comments, ~but I'll directly push those to this branch.~
> 
> Thanks again!
> 
> Update; I don't have time right now to make the comment changes. Let me know what you think @irenjj and if you are willing to make them in this PR. If not I'll make a follow on PR tomorrow.
> 
> Thanks again ❤️

Thanks @alamb for your review, I will revise it later according to your suggestions.
author_association: CONTRIBUTOR
user: login: jayzhan211
created_at: 2025-03-18T00:41:13Z
body: Is it possible to modify `Display` for Expr for explain statement?
author_association: CONTRIBUTOR
user: login: irenjj
created_at: 2025-03-18T11:53:51Z
body: > Is it possible to modify `Display` for Expr for explain statement?

I haven't tried it, not sure how it will affect other logic.
author_association: CONTRIBUTOR
user: login: irenjj
created_at: 2025-03-18T13:36:23Z
body: Thanks @alamb, @jayzhan211 and @xudong963 for your review, here are two points that remain unclear:
1. For GROUP BY, is it necessary to preserve the row index -- for more information, additional functionality, or just better aesthetics?
2. Should the default Display implementation of Expr be modified to replace SqlDisplay?
author_association: CONTRIBUTOR
user: login: alamb
created_at: 2025-03-18T19:17:37Z
body: 


> Thanks @alamb, @jayzhan211 and @xudong963 for your review, here are two points that remain unclear:
> 
> 1. For GROUP BY, is it necessary to preserve the row index -- for more information, additional functionality, or just better aesthetics?

I don't think we should keep the index for the tree explain plan

> Is it possible to modify Display for Expr for explain statement?

> 2. Should the default Display implementation of Expr be modified to replace SqlDisplay?

I think this would be a more disruptive change as it would change the existing `EXPLAIN ...` output. Thus I think we should consider it as a follow on PR


author_association: CONTRIBUTOR
user: login: alamb
created_at: 2025-03-18T19:24:03Z
body: I pushed a commit to add more documentation explaining the different options for printing `Expr`s

I also pushed a commit to rename `sql_name` to `human_display` as that seemed much more explanatory when I was writing the documentation

Let me know what you think
author_association: CONTRIBUTOR
user: login: alamb
created_at: 2025-03-19T19:29:11Z
body: I'll plan to merge this tomorrow unless others would like time to review
author_association: CONTRIBUTOR
user: login: alamb
created_at: 2025-03-20T14:54:34Z
body: Thank you @irenjj @jayzhan211 and @xudong963 🙏 
author_association: CONTRIBUTOR
user: login: berkaysynnada
created_at: 2025-03-28T12:57:44Z
body: @irenjj `AggregateFunctionExpr` has `with_new_expressions()` API. As datafusion hasn't implemented it yet, you didn't have difficulty rewriting the `human_display` according to the new expressions. Could you suggest a way to update `human_readable` field for the implementers of `with_new_expressions()`?
author_association: CONTRIBUTOR
user: login: irenjj
created_at: 2025-03-28T14:06:38Z
body: > @irenjj `AggregateFunctionExpr` has `with_new_expressions()` API. As datafusion hasn't implemented it yet, you didn't have difficulty rewriting the `human_display` according to the new expressions. Could you suggest a way to update `human_readable` field for the implementers of `with_new_expressions()`?

I believe that the `human_display` in `AggregateFunctionExpr` is constructed during the logical plan phase based on the available information at that time (and the `name` in `AggregateFunctionExpr` is similar). On the other hand, `with_new_expressions()` replaces the `args` in `AggregateFunctionExpr` during the physical plan phase (perhaps related to the optimizer?). It's perfectly fine to keep `human_display` unchanged when implementing `with_new_expressions()` because the `name` used in the original `indented explain` is actually quite similar to `human_display`.

Perhaps what we should really focus on is ensuring that the explain output correctly displays the `name` of the `expressions` after `with_new_expressions()` has been applied.
author_association: CONTRIBUTOR