[ 
https://issues.apache.org/jira/browse/SPARK-60329?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Max Gekk updated SPARK-60329:
-----------------------------
    Description: 
With whole-stage codegen enabled, {{FilterExec}} (and the {{FILTER}} clause of 
an aggregate function, through 
{{GeneratePredicateHelper.generatePredicateCode}}) emits a plain-column 
{{IsNotNull}} check lazily: just before the first conjunct that references its 
column, or after all conjuncts if none does. With whole-stage codegen disabled, 
{{And.eval}} evaluates the conjuncts in the order of the condition, where 
{{InferFiltersFromConstraints}} puts the inferred {{isnotnull(...)}} checks 
first and a user-written {{IS NOT NULL}} stays where it was written.

So when a plain-column {{IS NOT NULL}} check is FALSE on a row and a conjunct 
that comes *later* in the condition but does not reference that column raises 
an error on the same row, codegen reaches the error that the interpreted path 
never evaluates. The query fails only with whole-stage codegen on.

Reproducer (ANSI mode):

{code:sql}
CREATE TABLE t USING parquet AS
  SELECT * FROM VALUES (NULL, 1, 2147483647, NULL), (0, 1, 0, 1) AS v(a, b, y, 
x);
SET spark.sql.parquet.filterPushdown=false;

SET spark.sql.codegen.wholeStage=true;
SELECT a FROM t WHERE x IS NOT NULL AND y + 1 > 0 AND x > 0;
-- [ARITHMETIC_OVERFLOW] overflow. ... SQLSTATE: 22003

SET spark.sql.codegen.wholeStage=false;
SELECT a FROM t WHERE x IS NOT NULL AND y + 1 > 0 AND x > 0;
-- 0
{code}

The optimized condition is {{isnotnull(y) AND isnotnull(x) AND (y + 1) > 0 AND 
x > 0}}. With codegen off, {{isnotnull(x)}} is FALSE on the first row and the 
row is dropped. With codegen on, {{isnotnull(x)}} is emitted just before {{x > 
0}}, after {{(y + 1) > 0}}, so the overflow is raised.

This is independent of SPARK-60315, which is about a NULL conjunct stopping the 
evaluation with codegen on. The fix for SPARK-60315 keeps the lazy placement 
when no conjunct can be NULL, and only preserves the position of the pending 
checks once a NULL conjunct flag is generated, so this shape still differs 
between the two modes after it.

A fix should emit each plain-column {{IsNotNull}} check at its position in the 
condition, in both {{generatePredicateCode}} and the CSE branch of 
{{FilterExec.doConsume}}, which mirror each other by hand. The lazy placement 
exists so that columns are not loaded until a conjunct needs them, so the 
performance effect of the change on filters with many inferred checks should be 
measured. Complex {{IsNotNull(expr)}} checks, whose inner expression can itself 
raise, need the same care.


A small cleanup can ride along with this fix. In both code paths, when a 
conjunct references a column whose direct {{IsNotNull}} check was already 
generated, the {{else if}} branch of the reference loop (it is not guarded by 
{{idx == -1}}) generates a redundant synthetic {{IsNotNull(r)}} for the same 
column, once per column. It is a harmless duplicate null check, pre-existing, 
and after SPARK-60315 it is routine for the first conjunct that references a 
column whose check was generated before it. The fix is to guard the synthetic 
check with {{idx == -1}}; it is left out of SPARK-60315 because it changes the 
generated code when no conjunct can be NULL.


  was:
With whole-stage codegen enabled, {{FilterExec}} (and the {{FILTER}} clause of 
an aggregate function, through 
{{GeneratePredicateHelper.generatePredicateCode}}) emits a plain-column 
{{IsNotNull}} check lazily: just before the first conjunct that references its 
column, or after all conjuncts if none does. With whole-stage codegen disabled, 
{{And.eval}} evaluates the conjuncts in the order of the condition, where 
{{InferFiltersFromConstraints}} puts the inferred {{isnotnull(...)}} checks 
first and a user-written {{IS NOT NULL}} stays where it was written.

So when a plain-column {{IS NOT NULL}} check is FALSE on a row and a conjunct 
that comes *later* in the condition but does not reference that column raises 
an error on the same row, codegen reaches the error that the interpreted path 
never evaluates. The query fails only with whole-stage codegen on.

Reproducer (ANSI mode):

{code:sql}
CREATE TABLE t USING parquet AS
  SELECT * FROM VALUES (NULL, 1, 2147483647, NULL), (0, 1, 0, 1) AS v(a, b, y, 
x);
SET spark.sql.parquet.filterPushdown=false;

SET spark.sql.codegen.wholeStage=true;
SELECT a FROM t WHERE x IS NOT NULL AND y + 1 > 0 AND x > 0;
-- [ARITHMETIC_OVERFLOW] overflow. ... SQLSTATE: 22003

SET spark.sql.codegen.wholeStage=false;
SELECT a FROM t WHERE x IS NOT NULL AND y + 1 > 0 AND x > 0;
-- 0
{code}

The optimized condition is {{isnotnull(y) AND isnotnull(x) AND (y + 1) > 0 AND 
x > 0}}. With codegen off, {{isnotnull(x)}} is FALSE on the first row and the 
row is dropped. With codegen on, {{isnotnull(x)}} is emitted just before {{x > 
0}}, after {{(y + 1) > 0}}, so the overflow is raised.

This is independent of SPARK-60315, which is about a NULL conjunct stopping the 
evaluation with codegen on. The fix for SPARK-60315 keeps the lazy placement 
when no conjunct can be NULL, and only preserves the position of the pending 
checks once a NULL conjunct flag is generated, so this shape still differs 
between the two modes after it.

A fix should emit each plain-column {{IsNotNull}} check at its position in the 
condition, in both {{generatePredicateCode}} and the CSE branch of 
{{FilterExec.doConsume}}, which mirror each other by hand. The lazy placement 
exists so that columns are not loaded until a conjunct needs them, so the 
performance effect of the change on filters with many inferred checks should be 
measured. Complex {{IsNotNull(expr)}} checks, whose inner expression can itself 
raise, need the same care.



> Whole-stage codegen of a filter checks a plain-column IS NOT NULL later than 
> its position in the condition
> ----------------------------------------------------------------------------------------------------------
>
>                 Key: SPARK-60329
>                 URL: https://issues.apache.org/jira/browse/SPARK-60329
>             Project: Spark
>          Issue Type: Bug
>          Components: SQL
>    Affects Versions: 4.2.0, 4.3.0, 5.0.0, 4.1.3, 4.0.4, 4.4.0
>            Reporter: Max Gekk
>            Priority: Major
>
> With whole-stage codegen enabled, {{FilterExec}} (and the {{FILTER}} clause 
> of an aggregate function, through 
> {{GeneratePredicateHelper.generatePredicateCode}}) emits a plain-column 
> {{IsNotNull}} check lazily: just before the first conjunct that references 
> its column, or after all conjuncts if none does. With whole-stage codegen 
> disabled, {{And.eval}} evaluates the conjuncts in the order of the condition, 
> where {{InferFiltersFromConstraints}} puts the inferred {{isnotnull(...)}} 
> checks first and a user-written {{IS NOT NULL}} stays where it was written.
> So when a plain-column {{IS NOT NULL}} check is FALSE on a row and a conjunct 
> that comes *later* in the condition but does not reference that column raises 
> an error on the same row, codegen reaches the error that the interpreted path 
> never evaluates. The query fails only with whole-stage codegen on.
> Reproducer (ANSI mode):
> {code:sql}
> CREATE TABLE t USING parquet AS
>   SELECT * FROM VALUES (NULL, 1, 2147483647, NULL), (0, 1, 0, 1) AS v(a, b, 
> y, x);
> SET spark.sql.parquet.filterPushdown=false;
> SET spark.sql.codegen.wholeStage=true;
> SELECT a FROM t WHERE x IS NOT NULL AND y + 1 > 0 AND x > 0;
> -- [ARITHMETIC_OVERFLOW] overflow. ... SQLSTATE: 22003
> SET spark.sql.codegen.wholeStage=false;
> SELECT a FROM t WHERE x IS NOT NULL AND y + 1 > 0 AND x > 0;
> -- 0
> {code}
> The optimized condition is {{isnotnull(y) AND isnotnull(x) AND (y + 1) > 0 
> AND x > 0}}. With codegen off, {{isnotnull(x)}} is FALSE on the first row and 
> the row is dropped. With codegen on, {{isnotnull(x)}} is emitted just before 
> {{x > 0}}, after {{(y + 1) > 0}}, so the overflow is raised.
> This is independent of SPARK-60315, which is about a NULL conjunct stopping 
> the evaluation with codegen on. The fix for SPARK-60315 keeps the lazy 
> placement when no conjunct can be NULL, and only preserves the position of 
> the pending checks once a NULL conjunct flag is generated, so this shape 
> still differs between the two modes after it.
> A fix should emit each plain-column {{IsNotNull}} check at its position in 
> the condition, in both {{generatePredicateCode}} and the CSE branch of 
> {{FilterExec.doConsume}}, which mirror each other by hand. The lazy placement 
> exists so that columns are not loaded until a conjunct needs them, so the 
> performance effect of the change on filters with many inferred checks should 
> be measured. Complex {{IsNotNull(expr)}} checks, whose inner expression can 
> itself raise, need the same care.
> A small cleanup can ride along with this fix. In both code paths, when a 
> conjunct references a column whose direct {{IsNotNull}} check was already 
> generated, the {{else if}} branch of the reference loop (it is not guarded by 
> {{idx == -1}}) generates a redundant synthetic {{IsNotNull(r)}} for the same 
> column, once per column. It is a harmless duplicate null check, pre-existing, 
> and after SPARK-60315 it is routine for the first conjunct that references a 
> column whose check was generated before it. The fix is to guard the synthetic 
> check with {{idx == -1}}; it is left out of SPARK-60315 because it changes 
> the generated code when no conjunct can be NULL.



--
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