Josh Rosen created SPARK-58429:
----------------------------------

             Summary: Uncorrelated IN-subquery selected with a global aggregate 
returns false/NULL instead of true when the input is empty
                 Key: SPARK-58429
                 URL: https://issues.apache.org/jira/browse/SPARK-58429
             Project: Spark
          Issue Type: Bug
          Components: SQL
    Affects Versions: 4.0.0
            Reporter: Josh Rosen


This is a report of a silent wrong-results bug present in Spark 4.0.0 and later 
(introduced by SPARK-47070), under default configuration.

 
When an uncorrelated {{{}IN{}}}/{{{}EXISTS{}}} subquery predicate is selected 
alongside a global aggregate, {{RewritePredicateSubquery}} plans an 
{{ExistenceJoin}} *below* the {{Aggregate}} and wraps the resulting {{exists}} 
attribute in {{{}first(){}}}, on the 
[rationale|https://github.com/apache/spark/blob/8df89f20fc5c800ef4935c1b49a7525ec9df8921/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala#L356-L357]
 that "the value of `exists` is functionally determined by grouping 
expressions, so applying any aggregate function is semantically safe."

 
That rationale is vacuous for a {*}global aggregate over empty input{*}: it 
produces its single output row from zero input rows, so there is no row for 
{{first()}} to take a value from, and {{first()}} returns its initial (null) 
buffer.

 
h3. Repro (default configuration):
{code:java}
SELECT count(*), 1 IN (SELECT id FROM range(1, 2)) FROM range(0);
-- outputs: 0, false        WRONG (expected: 0, true) {code}
 * {{range(1, 2)}} is the one-element set {{{}{1}{}}},
 *  {{SELECT 1 IN (SELECT id FROM range(1, 2))}} returns {{true}} on its own; 
 * {{range(0)}} is empty, so {{count(*)}} is 0 and the predicate's value does 
not depend on the outer input at all. 
 * PostgreSQL and DuckDB return {{true}} here. The same query over a non-empty 
outer input returns {{true}} correctly, so it is specifically the empty case 
that is buggy.

 
Here is the incorrectly-optimized plan for this query:
{code:java}
Aggregate [count(1) AS count(1), first(exists#, false) AS (1 IN (listquery()))]
+- Join ExistenceJoin(exists#)
   :- Range (0, 0, step=1)
   +- Project
      +- Filter (1 = id#)
         +- Range (1, 2, step=1) {code}
 
Potential fix: for a global aggregate (empty {{{}groupingExpressions{}}}), 
rewrite the {{ExistenceJoin}} _above_ the {{Aggregate}} so {{exists}} is 
evaluated on the aggregate's single output row. 



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

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

Reply via email to