You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by we...@apache.org on 2025/03/14 23:43:31 UTC

(spark) branch master updated: [SPARK-51501][SQL] Disable ObjectHashAggregate for group by on collated columns

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

wenchen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new f7495da2da8e [SPARK-51501][SQL] Disable ObjectHashAggregate for group by on collated columns
f7495da2da8e is described below

commit f7495da2da8e2662c8746d44dd5f18de9ed3e982
Author: Stefan Kandic <st...@databricks.com>
AuthorDate: Fri Mar 14 16:43:16 2025 -0700

    [SPARK-51501][SQL] Disable ObjectHashAggregate for group by on collated columns
    
    ### What changes were proposed in this pull request?
    Disabling `ObjectHashAggregate` when grouping on columns with collations.
    
    ### Why are the changes needed?
    https://github.com/apache/spark/pull/45290 added support for sort based aggregation on collated columns and explicitly forbade the use of hash aggregate for collated columns. However, it did not consider the third type of aggregate, the object hash aggregate, which is only used when there are also TypedImperativeAggregate expressions present ([source](https://github.com/apache/spark/blob/f3b081066393e1568c364b6d3bc0bceabd1e7e9f/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst [...]
    
    That means that if we group by a collated column and also have a TypedImperativeAggregate we will end up using the object has aggregate which can lead to incorrect results like in the example below:
    
    ```code
    CREATE TABLE tbl(c1 STRING COLLATE UTF8_LCASE, c2 INT) USING PARQUET;
    INSERT INTO tbl VALUES ('HELLO', 1), ('hello', 2), ('HeLlO', 3);
    SELECT COLLECT_LIST(c2) as list FROM tbl GROUP BY c1;
    ```
    where the result would have three rows with values [1], [2] and [3] instead of one row with value [1, 2, 3].
    
    For this reason we should do the same thing as we did for the regular hash aggregate, make it so that it doesn't support grouping expressions on collated columns.
    
    ### Does this PR introduce _any_ user-facing change?
    No.
    
    ### How was this patch tested?
    New unit tests.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    No.
    
    Closes #50267 from stefankandic/fixObjectHashAgg.
    
    Authored-by: Stefan Kandic <st...@databricks.com>
    Signed-off-by: Wenchen Fan <we...@databricks.com>
---
 .../catalyst/optimizer/MergeScalarSubqueries.scala |  6 +-
 .../plans/logical/basicLogicalOperators.scala      |  9 ++-
 .../spark/sql/execution/aggregate/AggUtils.scala   |  3 +-
 .../sql/collation/CollationAggregationSuite.scala  | 90 ++++++++++++++++++++++
 4 files changed, 104 insertions(+), 4 deletions(-)

diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/MergeScalarSubqueries.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/MergeScalarSubqueries.scala
index 2d1e71a63a8c..eb95bef53739 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/MergeScalarSubqueries.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/MergeScalarSubqueries.scala
@@ -366,8 +366,10 @@ object MergeScalarSubqueries extends Rule[LogicalPlan] {
     newPlanSupportsHashAggregate && cachedPlanSupportsHashAggregate ||
       newPlanSupportsHashAggregate == cachedPlanSupportsHashAggregate && {
         val Seq(newPlanSupportsObjectHashAggregate, cachedPlanSupportsObjectHashAggregate) =
-          aggregateExpressionsSeq.map(aggregateExpressions =>
-            Aggregate.supportsObjectHashAggregate(aggregateExpressions))
+          aggregateExpressionsSeq.zip(groupByExpressionSeq).map {
+            case (aggregateExpressions, groupByExpressions) =>
+              Aggregate.supportsObjectHashAggregate(aggregateExpressions, groupByExpressions)
+          }
         newPlanSupportsObjectHashAggregate && cachedPlanSupportsObjectHashAggregate ||
           newPlanSupportsObjectHashAggregate == cachedPlanSupportsObjectHashAggregate
       }
diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
index 7d279092668f..caff207d28a1 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
@@ -1199,7 +1199,14 @@ object Aggregate {
       groupingExpression.forall(e => UnsafeRowUtils.isBinaryStable(e.dataType))
   }
 
-  def supportsObjectHashAggregate(aggregateExpressions: Seq[AggregateExpression]): Boolean = {
+  def supportsObjectHashAggregate(
+      aggregateExpressions: Seq[AggregateExpression],
+      groupingExpressions: Seq[Expression]): Boolean = {
+    // We should not use hash aggregation on binary unstable types.
+    if (groupingExpressions.exists(e => !UnsafeRowUtils.isBinaryStable(e.dataType))) {
+      return false
+    }
+
     aggregateExpressions.map(_.aggregateFunction).exists {
       case _: TypedImperativeAggregate[_] => true
       case _ => false
diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggUtils.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggUtils.scala
index 367d4cfafb48..de1b83c16ac9 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggUtils.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggUtils.scala
@@ -94,7 +94,8 @@ object AggUtils {
         child = child)
     } else {
       val objectHashEnabled = child.conf.useObjectHashAggregation
-      val useObjectHash = Aggregate.supportsObjectHashAggregate(aggregateExpressions)
+      val useObjectHash = Aggregate.supportsObjectHashAggregate(
+        aggregateExpressions, groupingExpressions)
 
       if (forceObjHashAggregate || (objectHashEnabled && useObjectHash && !forceSortAggregate)) {
         ObjectHashAggregateExec(
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/collation/CollationAggregationSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/collation/CollationAggregationSuite.scala
new file mode 100644
index 000000000000..e1a6c9a3dbfc
--- /dev/null
+++ b/sql/core/src/test/scala/org/apache/spark/sql/collation/CollationAggregationSuite.scala
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.collation
+
+import org.apache.spark.sql.{QueryTest, Row}
+import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
+import org.apache.spark.sql.execution.aggregate.{HashAggregateExec, ObjectHashAggregateExec, SortAggregateExec}
+import org.apache.spark.sql.test.SharedSparkSession
+
+class CollationAggregationSuite
+  extends QueryTest
+  with SharedSparkSession
+  with AdaptiveSparkPlanHelper {
+
+  test("group by collated column doesn't work with obj hash aggregate") {
+    val tblName = "grp_by_tbl"
+    withTable(tblName) {
+      sql(s"CREATE TABLE $tblName (c1 STRING COLLATE UTF8_LCASE, c2 INT) USING PARQUET")
+      sql(s"INSERT INTO $tblName VALUES ('hello', 1), ('HELLO', 2), ('HeLlO', 3)")
+
+      // Result is correct without forcing object hash aggregate.
+      checkAnswer(
+        sql(s"SELECT COUNT(*) FROM $tblName GROUP BY c1"),
+        Seq(Row(3)))
+
+      withSQLConf("spark.sql.test.forceApplyObjectHashAggregate" -> true.toString) {
+        checkAnswer(
+          sql(s"SELECT COUNT(*) FROM $tblName GROUP BY c1"),
+          Seq(Row(1), Row(1), Row(1)))
+
+        checkAnswer(
+          sql(s"SELECT COLLECT_LIST(c2) AS c3 FROM $tblName GROUP BY c1 ORDER BY c3"),
+          Seq(Row(Seq(1)), Row(Seq(2)), Row(Seq(3))))
+      }
+    }
+  }
+
+  test("imperative aggregate fn does not use objectHashAggregate when group by collated column") {
+    val tblName = "imp_agg"
+    Seq(true, false).foreach { useObjHashAgg =>
+      withTable(tblName) {
+        withSQLConf("spark.sql.execution.useObjectHashAggregateExec" -> useObjHashAgg.toString) {
+          sql(
+            s"""
+               |CREATE TABLE $tblName (
+               |  c1 STRING COLLATE UTF8_LCASE,
+               |  c2 INT
+               |) USING PARQUET
+               |""".stripMargin)
+          sql(s"INSERT INTO $tblName VALUES ('HELLO', 1), ('hello', 2), ('HeLlO', 3)")
+
+          val df = sql(s"SELECT COLLECT_LIST(c2) as list FROM $tblName GROUP BY c1")
+          val executedPlan = df.queryExecution.executedPlan
+
+          // Plan should not have any hash aggregate nodes.
+          collectFirst(executedPlan) {
+            case _: ObjectHashAggregateExec => fail("ObjectHashAggregateExec should not be used.")
+            case _: HashAggregateExec => fail("HashAggregateExec should not be used.")
+          }
+
+          // Plan should have a [[SortAggregateExec]] node.
+          assert(collectFirst(executedPlan) {
+            case _: SortAggregateExec => true
+          }.nonEmpty)
+
+          checkAnswer(
+            // Sort the values to get deterministic output.
+            df.selectExpr("array_sort(list)"),
+            Seq(Row(Seq(1, 2, 3)))
+          )
+        }
+      }
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org