Repository: spark
Updated Branches:
  refs/heads/branch-2.1 50f28dfe4 -> 8e097890a


[SPARK-20686][SQL] PropagateEmptyRelation incorrectly handles aggregate without 
grouping

The query

```
SELECT 1 FROM (SELECT COUNT(*) WHERE FALSE) t1
```

should return a single row of output because the subquery is an aggregate 
without a group-by and thus should return a single row. However, Spark 
incorrectly returns zero rows.

This is caused by SPARK-16208 / #13906, a patch which added an optimizer rule 
to propagate EmptyRelation through operators. The logic for handling aggregates 
is wrong: it checks whether aggregate expressions are non-empty for deciding 
whether the output should be empty, whereas it should be checking grouping 
expressions instead:

An aggregate with non-empty grouping expression will return one output row per 
group. If the input to the grouped aggregate is empty then all groups will be 
empty and thus the output will be empty. It doesn't matter whether the 
aggregation output columns include aggregate expressions since that won't 
affect the number of output rows.

If the grouping expressions are empty, however, then the aggregate will always 
produce a single output row and thus we cannot propagate the EmptyRelation.

The current implementation is incorrect and also misses an optimization 
opportunity by not propagating EmptyRelation in the case where a grouped 
aggregate has aggregate expressions (in other words, `SELECT COUNT(*) from 
emptyRelation GROUP BY x` would _not_ be optimized to `EmptyRelation` in the 
old code, even though it safely could be).

This patch resolves this issue by modifying `PropagateEmptyRelation` to 
consider only the presence/absence of grouping expressions, not the aggregate 
functions themselves, when deciding whether to propagate EmptyRelation.

- Added end-to-end regression tests in `SQLQueryTest`'s `group-by.sql` file.
- Updated unit tests in `PropagateEmptyRelationSuite`.

Author: Josh Rosen <[email protected]>

Closes #17929 from JoshRosen/fix-PropagateEmptyRelation.

(cherry picked from commit a90c5cd8226146a58362732171b92cb99a7bc4c7)
Signed-off-by: Wenchen Fan <[email protected]>


Project: http://git-wip-us.apache.org/repos/asf/spark/repo
Commit: http://git-wip-us.apache.org/repos/asf/spark/commit/8e097890
Tree: http://git-wip-us.apache.org/repos/asf/spark/tree/8e097890
Diff: http://git-wip-us.apache.org/repos/asf/spark/diff/8e097890

Branch: refs/heads/branch-2.1
Commit: 8e097890a7b39cd8320ed2f2b98dc2b520a87cbf
Parents: 50f28df
Author: Josh Rosen <[email protected]>
Authored: Wed May 10 14:36:36 2017 +0800
Committer: Wenchen Fan <[email protected]>
Committed: Wed May 10 14:41:46 2017 +0800

----------------------------------------------------------------------
 .../optimizer/PropagateEmptyRelation.scala      | 16 ++++++------
 .../optimizer/PropagateEmptyRelationSuite.scala |  8 +++---
 .../resources/sql-tests/inputs/group-by.sql     |  7 ++++++
 .../sql-tests/results/group-by.sql.out          | 26 +++++++++++++++++++-
 4 files changed, 44 insertions(+), 13 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/spark/blob/8e097890/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelation.scala
----------------------------------------------------------------------
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelation.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelation.scala
index 7400a01..987cd74 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelation.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelation.scala
@@ -18,7 +18,6 @@
 package org.apache.spark.sql.catalyst.optimizer
 
 import org.apache.spark.sql.catalyst.expressions._
-import org.apache.spark.sql.catalyst.expressions.aggregate.AggregateFunction
 import org.apache.spark.sql.catalyst.plans._
 import org.apache.spark.sql.catalyst.plans.logical._
 import org.apache.spark.sql.catalyst.rules._
@@ -30,7 +29,7 @@ import org.apache.spark.sql.catalyst.rules._
  *    - Join with one or two empty children (including Intersect/Except).
  * 2. Unary-node Logical Plans
  *    - Project/Filter/Sample/Join/Limit/Repartition with all empty children.
- *    - Aggregate with all empty children and without AggregateFunction 
expressions like COUNT.
+ *    - Aggregate with all empty children and at least one grouping expression.
  *    - Generate(Explode) with all empty children. Others like Hive UDTF may 
return results.
  */
 object PropagateEmptyRelation extends Rule[LogicalPlan] with PredicateHelper {
@@ -39,10 +38,6 @@ object PropagateEmptyRelation extends Rule[LogicalPlan] with 
PredicateHelper {
     case _ => false
   }
 
-  private def containsAggregateExpression(e: Expression): Boolean = {
-    e.collectFirst { case _: AggregateFunction => () }.isDefined
-  }
-
   private def empty(plan: LogicalPlan) = LocalRelation(plan.output, data = 
Seq.empty)
 
   def apply(plan: LogicalPlan): LogicalPlan = plan transformUp {
@@ -68,8 +63,13 @@ object PropagateEmptyRelation extends Rule[LogicalPlan] with 
PredicateHelper {
       case _: LocalLimit => empty(p)
       case _: Repartition => empty(p)
       case _: RepartitionByExpression => empty(p)
-      // AggregateExpressions like COUNT(*) return their results like 0.
-      case Aggregate(_, ae, _) if !ae.exists(containsAggregateExpression) => 
empty(p)
+      // An aggregate with non-empty group expression will return one output 
row per group when the
+      // input to the aggregate is not empty. If the input to the aggregate is 
empty then all groups
+      // will be empty and thus the output will be empty.
+      //
+      // If the grouping expressions are empty, however, then the aggregate 
will always produce a
+      // single output row and thus we cannot propagate the EmptyRelation.
+      case Aggregate(ge, _, _) if ge.nonEmpty => empty(p)
       // Generators like Hive-style UDTF may return their records within 
`close`.
       case Generate(_: Explode, _, _, _, _, _) => empty(p)
       case _ => p

http://git-wip-us.apache.org/repos/asf/spark/blob/8e097890/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelationSuite.scala
----------------------------------------------------------------------
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelationSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelationSuite.scala
index 908dde7..2285be1 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelationSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/PropagateEmptyRelationSuite.scala
@@ -142,7 +142,7 @@ class PropagateEmptyRelationSuite extends PlanTest {
     comparePlans(optimized, correctAnswer.analyze)
   }
 
-  test("propagate empty relation through Aggregate without aggregate 
function") {
+  test("propagate empty relation through Aggregate with grouping expressions") 
{
     val query = testRelation1
       .where(false)
       .groupBy('a)('a, ('a + 1).as('x))
@@ -153,13 +153,13 @@ class PropagateEmptyRelationSuite extends PlanTest {
     comparePlans(optimized, correctAnswer)
   }
 
-  test("don't propagate empty relation through Aggregate with aggregate 
function") {
+  test("don't propagate empty relation through Aggregate without grouping 
expressions") {
     val query = testRelation1
       .where(false)
-      .groupBy('a)(count('a))
+      .groupBy()()
 
     val optimized = Optimize.execute(query.analyze)
-    val correctAnswer = LocalRelation('a.int).groupBy('a)(count('a)).analyze
+    val correctAnswer = LocalRelation('a.int).groupBy()().analyze
 
     comparePlans(optimized, correctAnswer)
   }

http://git-wip-us.apache.org/repos/asf/spark/blob/8e097890/sql/core/src/test/resources/sql-tests/inputs/group-by.sql
----------------------------------------------------------------------
diff --git a/sql/core/src/test/resources/sql-tests/inputs/group-by.sql 
b/sql/core/src/test/resources/sql-tests/inputs/group-by.sql
index 4d0ed43..f4f5a04 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/group-by.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/group-by.sql
@@ -35,3 +35,10 @@ FROM testData;
 
 -- Aggregate with foldable input and multiple distinct groups.
 SELECT COUNT(DISTINCT b), COUNT(DISTINCT b, c) FROM (SELECT 1 AS a, 2 AS b, 3 
AS c) GROUP BY a;
+
+-- Aggregate with empty input and non-empty GroupBy expressions.
+SELECT a, COUNT(1) FROM testData WHERE false GROUP BY a;
+
+-- Aggregate with empty input and empty GroupBy expressions.
+SELECT COUNT(1) FROM testData WHERE false;
+SELECT 1 FROM (SELECT COUNT(1) FROM testData WHERE false) t;

http://git-wip-us.apache.org/repos/asf/spark/blob/8e097890/sql/core/src/test/resources/sql-tests/results/group-by.sql.out
----------------------------------------------------------------------
diff --git a/sql/core/src/test/resources/sql-tests/results/group-by.sql.out 
b/sql/core/src/test/resources/sql-tests/results/group-by.sql.out
index 4b87d51..f64dd00 100644
--- a/sql/core/src/test/resources/sql-tests/results/group-by.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/group-by.sql.out
@@ -1,5 +1,5 @@
 -- Automatically generated by SQLQueryTestSuite
--- Number of queries: 15
+-- Number of queries: 18
 
 
 -- !query 0
@@ -139,3 +139,27 @@ SELECT COUNT(DISTINCT b), COUNT(DISTINCT b, c) FROM 
(SELECT 1 AS a, 2 AS b, 3 AS
 struct<count(DISTINCT b):bigint,count(DISTINCT b, c):bigint>
 -- !query 14 output
 1      1
+
+
+-- !query 15
+SELECT a, COUNT(1) FROM testData WHERE false GROUP BY a
+-- !query 15 schema
+struct<a:int,count(1):bigint>
+-- !query 15 output
+
+
+
+-- !query 16
+SELECT COUNT(1) FROM testData WHERE false
+-- !query 16 schema
+struct<count(1):bigint>
+-- !query 16 output
+0
+
+
+-- !query 17
+SELECT 1 FROM (SELECT COUNT(1) FROM testData WHERE false) t
+-- !query 17 schema
+struct<1:int>
+-- !query 17 output
+1


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

Reply via email to