sarutak commented on code in PR #59940:
URL: https://github.com/apache/spark/pull/59940#discussion_r4469544808
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2642,15 +2650,69 @@ object PushPredicateThroughNonJoin extends
Rule[LogicalPlan] with PredicateHelpe
// eliminate more data quickly and it may also be something which
could be evaluated at
// the storage layer. We may wish to improve this heuristic in the
future.
def isCheap(used: AttributeMap[Alias]): Boolean = !used.exists(a =>
isExpensive(a._2.child))
+ // Whether a component reads an expensive alias that this projection
evaluates. A scalar
+ // Python UDF is not evaluated here: `ExtractPythonUDFs` later moves
it, with its
+ // arguments, to an eval node below the projection, and pushes the
conditions below that
+ // again, except the throwable ones written after one over its output
(SPARK-60633), so
+ // holding a condition back behind one here would gain nothing and
only cost
+ // the pushdown of the batches in between. What is left of the alias
without its Python
+ // UDFs, e.g. a Scala UDF over one, is evaluated here though. Each
Python UDF is replaced
+ // with an attribute, as `ExtractPythonUDFs` does, so that
`isExpensive` sees an input
+ // that is not foldable, as in the projection it leaves.
+ def evaluatedHere(used: AttributeMap[Alias]): Boolean = used.exists {
case (_, alias) =>
+ isExpensive(alias.child) && isExpensive(alias.child.transformDown {
+ case udf if isScalarPythonUDF(udf) =>
AttributeReference("pythonUDF", udf.dataType)()
+ })
+ }
// Split the filter's components into cheap and expensive while
keeping track of
// what each references from the projection. The cheap components
written before the
- // first expensive one are all pushed. After it, a throwable component
(e.g.
- // `raise_error`, possibly inside a projected alias) stays above with
the expensive ones,
- // so it is not evaluated on rows the components before it would have
filtered out.
- val (cheapWithUsed, expensiveWithUsed) = splitPushable(
- originalAndRewrittenConditionsWithUsedAlias)(
- canPush = { case (_, used, _) => isCheap(used) },
- throwable = { case (_, _, replaced) => replaced.throwable })
+ // first expensive one are pushed, except the IS NOT NULL checks kept
above and the
+ // throwable components written after one (see below).
+ // After the first component over an expensive alias the projection
evaluates, which is
+ // the first one held back, only the cheap components that
`canPushAhead` accepts are
+ // pushed; the others stay above with the expensive ones, so they are
only evaluated
+ // where none of the components before them is false. Before that one,
the cheap
+ // components that are not throwable are pushed, as before
SPARK-60730, except the IS NOT
+ // NULL checks kept above.
+ val (written, rest) = originalAndRewrittenConditionsWithUsedAlias.span
{
+ case (_, used, _) => isCheap(used)
+ }
+ val (beforeHeld, fromHeld) = rest.span {
+ case (_, used, _) => isCheap(used) || !evaluatedHere(used)
+ }
+ val (firstHeld, held) = fromHeld.splitAt(1)
+ // `InferFiltersFromConstraints` puts the IS NOT NULL checks it infers
in front of the
+ // condition, so such a check over a cheap alias that a held component
after the first
+ // one reads, e.g. a division, would be pushed and compute the alias
ahead of the
+ // components before that one. It stays above when `canPushAhead`
rejects it with the
+ // alias substituted, also when the user wrote it there. With
whole-stage codegen,
+ // `FilterExec` then evaluates an IS NOT NULL check on a column just
before the first
+ // component that reads the column, or earlier after a component that
can be NULL (see
+ // `precedingNotNullChecks`). A held component that reads the alias is
rejected
+ // too, so the projection is not split then (see `keepsWrittenOrder`).
+ val heldRefs = AttributeSet(held.flatMap(_._1.references))
+ def keepsNotNull(cond: Expression, used: AttributeMap[Alias],
replaced: Expression) =
+ cond.isInstanceOf[IsNotNull] && used.keys.exists(heldRefs.contains)
&&
+ !canPushAhead(replaced)
+ // As in `splitPushable`, a throwable component written after a check
kept above stays
+ // above too.
+ val (beforeKept, fromKept) = written.span {
+ case (cond, used, replaced) => !keepsNotNull(cond, used, replaced)
+ }
+ val (keptWritten, afterKept) = fromKept.partition {
+ case (cond, used, replaced) => keepsNotNull(cond, used, replaced) ||
replaced.throwable
+ }
+ val leading = beforeKept ++ afterKept
Review Comment:
Reproduced: `id / b > 1` was pushed alone over the `Range` and failed on
`id` 0. I first tried your suggestion, and the query then still failed with
whole-stage codegen. The filter above the projection was `isnotnull(x) AND id /
b > 1 AND f > 5 AND x < 10`, in the written order, but `FilterExec` emits an
`IS NOT NULL` check on a column just before the first conjunct that reads the
column, here `x < 10`, so the division, which does not read `x`, ran first.
Without codegen it returned rows 7 and 8. 4.3.0 and master push the check as
`isnotnull(try_divide(id, id % 3))`, which `FilterExec` evaluates in its
written position. So keeping a check above can not keep its written order for
the conditions after it that do not read the column, and this rule can not tell
a written check from an inferred one. I removed the kept check: a check
written, or inferred, before the first expensive condition is pushed with the
conditions after it as on master, so your query returns rows 7 and 8 in every
combination of codegen, the split and `avoidDoubleFilterEval`; it is in the
`DataFrameSuite` loop now, and `FilterPushdownSuite` checks the pushed order.
This brings back the case of the second round's items 1 and 6, an inferred
`isnotnull(x)` over a cheap alias that can fail, which also fails on master. So
I took out the end-to-end queries and the description's table row for the
division and the cast inside a projected alias that I added for item 6, and
named the case in the description as not covered here. I think item 3 is the
right fix for it.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2642,15 +2650,69 @@ object PushPredicateThroughNonJoin extends
Rule[LogicalPlan] with PredicateHelpe
// eliminate more data quickly and it may also be something which
could be evaluated at
// the storage layer. We may wish to improve this heuristic in the
future.
def isCheap(used: AttributeMap[Alias]): Boolean = !used.exists(a =>
isExpensive(a._2.child))
+ // Whether a component reads an expensive alias that this projection
evaluates. A scalar
+ // Python UDF is not evaluated here: `ExtractPythonUDFs` later moves
it, with its
+ // arguments, to an eval node below the projection, and pushes the
conditions below that
+ // again, except the throwable ones written after one over its output
(SPARK-60633), so
+ // holding a condition back behind one here would gain nothing and
only cost
+ // the pushdown of the batches in between. What is left of the alias
without its Python
+ // UDFs, e.g. a Scala UDF over one, is evaluated here though. Each
Python UDF is replaced
+ // with an attribute, as `ExtractPythonUDFs` does, so that
`isExpensive` sees an input
+ // that is not foldable, as in the projection it leaves.
+ def evaluatedHere(used: AttributeMap[Alias]): Boolean = used.exists {
case (_, alias) =>
+ isExpensive(alias.child) && isExpensive(alias.child.transformDown {
+ case udf if isScalarPythonUDF(udf) =>
AttributeReference("pythonUDF", udf.dataType)()
+ })
+ }
// Split the filter's components into cheap and expensive while
keeping track of
// what each references from the projection. The cheap components
written before the
- // first expensive one are all pushed. After it, a throwable component
(e.g.
- // `raise_error`, possibly inside a projected alias) stays above with
the expensive ones,
- // so it is not evaluated on rows the components before it would have
filtered out.
- val (cheapWithUsed, expensiveWithUsed) = splitPushable(
- originalAndRewrittenConditionsWithUsedAlias)(
- canPush = { case (_, used, _) => isCheap(used) },
- throwable = { case (_, _, replaced) => replaced.throwable })
+ // first expensive one are pushed, except the IS NOT NULL checks kept
above and the
+ // throwable components written after one (see below).
+ // After the first component over an expensive alias the projection
evaluates, which is
+ // the first one held back, only the cheap components that
`canPushAhead` accepts are
+ // pushed; the others stay above with the expensive ones, so they are
only evaluated
+ // where none of the components before them is false. Before that one,
the cheap
+ // components that are not throwable are pushed, as before
SPARK-60730, except the IS NOT
+ // NULL checks kept above.
+ val (written, rest) = originalAndRewrittenConditionsWithUsedAlias.span
{
+ case (_, used, _) => isCheap(used)
+ }
+ val (beforeHeld, fromHeld) = rest.span {
+ case (_, used, _) => isCheap(used) || !evaluatedHere(used)
+ }
+ val (firstHeld, held) = fromHeld.splitAt(1)
+ // `InferFiltersFromConstraints` puts the IS NOT NULL checks it infers
in front of the
+ // condition, so such a check over a cheap alias that a held component
after the first
+ // one reads, e.g. a division, would be pushed and compute the alias
ahead of the
+ // components before that one. It stays above when `canPushAhead`
rejects it with the
+ // alias substituted, also when the user wrote it there. With
whole-stage codegen,
+ // `FilterExec` then evaluates an IS NOT NULL check on a column just
before the first
+ // component that reads the column, or earlier after a component that
can be NULL (see
+ // `precedingNotNullChecks`). A held component that reads the alias is
rejected
+ // too, so the projection is not split then (see `keepsWrittenOrder`).
+ val heldRefs = AttributeSet(held.flatMap(_._1.references))
+ def keepsNotNull(cond: Expression, used: AttributeMap[Alias],
replaced: Expression) =
+ cond.isInstanceOf[IsNotNull] && used.keys.exists(heldRefs.contains)
&&
+ !canPushAhead(replaced)
+ // As in `splitPushable`, a throwable component written after a check
kept above stays
+ // above too.
+ val (beforeKept, fromKept) = written.span {
+ case (cond, used, replaced) => !keepsNotNull(cond, used, replaced)
+ }
+ val (keptWritten, afterKept) = fromKept.partition {
+ case (cond, used, replaced) => keepsNotNull(cond, used, replaced) ||
replaced.throwable
+ }
+ val leading = beforeKept ++ afterKept
+ val (cheapBefore, expensiveBefore) = beforeHeld.partition {
+ case (cond, used, replaced) =>
+ isCheap(used) && !replaced.throwable && !keepsNotNull(cond, used,
replaced)
Review Comment:
Reproduced: 29 rows, with `rand(10) > 0.5` directly over the `Range`. Done
as you suggest: after a condition over an alias that is only expensive through
Python UDFs, a cheap condition is now pushed only when it is deterministic and
not throwable. Your query now returns the 31 rows of 4.1, with the split on and
off: `rand(10) > 0.5` stays after `p >= 50`, and after `ExtractPythonUDFs` both
are evaluated in that order over `BatchEvalPython`. As `rand(10) > 0.5` is kept
above there, the projection is not split either, also when no condition is held
back, so the split does not move a later expensive condition, over a Python UDF
or over an alias the projection evaluates, below it. The split still does so
below a non-deterministic condition that is itself over an expensive alias,
such as `f > rand(10) && g > 6` with `f` and `g` UDFs, as on master since
SPARK-55014; SPARK-60731, which makes the split follow the written order, fixes
that, and the description names it. The description and
the config doc now say that a non-deterministic condition after a condition
over a Python UDF stays above too; the legacy config pushes it again. The
migration guide entry leaves the Python UDF case out, since a Python UDF is
only expensive since SPARK-59693, which is not in a release yet.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2642,15 +2650,69 @@ object PushPredicateThroughNonJoin extends
Rule[LogicalPlan] with PredicateHelpe
// eliminate more data quickly and it may also be something which
could be evaluated at
// the storage layer. We may wish to improve this heuristic in the
future.
def isCheap(used: AttributeMap[Alias]): Boolean = !used.exists(a =>
isExpensive(a._2.child))
+ // Whether a component reads an expensive alias that this projection
evaluates. A scalar
+ // Python UDF is not evaluated here: `ExtractPythonUDFs` later moves
it, with its
+ // arguments, to an eval node below the projection, and pushes the
conditions below that
+ // again, except the throwable ones written after one over its output
(SPARK-60633), so
+ // holding a condition back behind one here would gain nothing and
only cost
+ // the pushdown of the batches in between. What is left of the alias
without its Python
+ // UDFs, e.g. a Scala UDF over one, is evaluated here though. Each
Python UDF is replaced
+ // with an attribute, as `ExtractPythonUDFs` does, so that
`isExpensive` sees an input
+ // that is not foldable, as in the projection it leaves.
+ def evaluatedHere(used: AttributeMap[Alias]): Boolean = used.exists {
case (_, alias) =>
+ isExpensive(alias.child) && isExpensive(alias.child.transformDown {
+ case udf if isScalarPythonUDF(udf) =>
AttributeReference("pythonUDF", udf.dataType)()
+ })
+ }
// Split the filter's components into cheap and expensive while
keeping track of
// what each references from the projection. The cheap components
written before the
- // first expensive one are all pushed. After it, a throwable component
(e.g.
- // `raise_error`, possibly inside a projected alias) stays above with
the expensive ones,
- // so it is not evaluated on rows the components before it would have
filtered out.
- val (cheapWithUsed, expensiveWithUsed) = splitPushable(
- originalAndRewrittenConditionsWithUsedAlias)(
- canPush = { case (_, used, _) => isCheap(used) },
- throwable = { case (_, _, replaced) => replaced.throwable })
+ // first expensive one are pushed, except the IS NOT NULL checks kept
above and the
+ // throwable components written after one (see below).
+ // After the first component over an expensive alias the projection
evaluates, which is
+ // the first one held back, only the cheap components that
`canPushAhead` accepts are
+ // pushed; the others stay above with the expensive ones, so they are
only evaluated
+ // where none of the components before them is false. Before that one,
the cheap
+ // components that are not throwable are pushed, as before
SPARK-60730, except the IS NOT
+ // NULL checks kept above.
+ val (written, rest) = originalAndRewrittenConditionsWithUsedAlias.span
{
+ case (_, used, _) => isCheap(used)
+ }
+ val (beforeHeld, fromHeld) = rest.span {
+ case (_, used, _) => isCheap(used) || !evaluatedHere(used)
+ }
+ val (firstHeld, held) = fromHeld.splitAt(1)
+ // `InferFiltersFromConstraints` puts the IS NOT NULL checks it infers
in front of the
+ // condition, so such a check over a cheap alias that a held component
after the first
+ // one reads, e.g. a division, would be pushed and compute the alias
ahead of the
+ // components before that one. It stays above when `canPushAhead`
rejects it with the
+ // alias substituted, also when the user wrote it there. With
whole-stage codegen,
Review Comment:
Agreed, and item 1 shows why the checks are better handled where they are
inferred than here. I would like to do it in a separate PR, since it changes
the conjunct order of the plans with inferred filters, and so the plan
stability golden files, and it is independent of this one. Until then the
inferred checks are pushed as on master, which the description now names.
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/FilterPushdownSuite.scala:
##########
@@ -984,6 +984,461 @@ class FilterPushdownSuite extends PlanTest {
comparePlans(Optimize.execute(originalQuery.analyze),
correctAnswer.analyze)
}
+ test("SPARK-60730: a cheap condition that can fail is not pushed ahead of an
expensive one") {
+ Seq(true, false).foreach { split =>
+ withSQLConf(SQLConf.SPLIT_PROJECTION_FOR_EXPENSIVE_FILTERS.key ->
split.toString) {
+ val originalQuery = testStringRelation
+ .select($"a", expensiveRLike("magic") as "f", $"b")
+ .where($"f" && $"a" + 1 > 6 && $"b" > 1 && $"e".cast(IntegerType) >
0 &&
+ IsNotNull($"a"))
+ .analyze
+ val Seq(expensive, arithmetic, _, cast, _) =
splitConjunctivePredicates(
+ originalQuery.collectFirst { case Filter(condition, _) => condition
}.get)
+
+ val optimized = Optimize.execute(originalQuery)
+
+ // Only the conditions that can not fail move below the expensive one.
The others stay
+ // above it in their written order, also with the split, which would
otherwise put
+ // them lowest since they need no alias.
+ val correctAnswer = testStringRelation
+ .where($"b" > 1 && IsNotNull($"a"))
+ .select($"a", expensiveRLike("magic") as "f", $"b")
+ .where($"f" && $"a" + 1 > 6 && $"e".cast(IntegerType) > 0)
+ .analyze
+
+ comparePlans(optimized, correctAnswer)
+ assertTopFilterConjuncts(optimized, Seq(expensive, arithmetic, cast))
+
+ // Written ahead of every expensive condition, they are pushed as
usual.
+ val failableFirst = testStringRelation
+ .select($"a", expensiveRLike("magic") as "f", $"b")
+ .where($"a" + 1 > 6 && $"f")
+ .analyze
+
+ val failableFirstAnswer = testStringRelation
+ .where($"a" + 1 > 6)
+ .select($"a", expensiveRLike("magic") as "f", $"b")
+ .where($"f")
+ .analyze
+
+ comparePlans(Optimize.execute(failableFirst), failableFirstAnswer)
+ }
+ }
+ }
+
+ test("SPARK-60730: a cheap condition over an alias that can fail is not
pushed ahead of an " +
+ "expensive one") {
+ val originalQuery = testStringRelation
+ .select($"a", expensiveRLike("magic") as "f", $"a" + 1 as "x", $"b")
+ .where($"f" && $"x" > 6 && $"b" > 1)
+ .analyze
+
+ val optimized = Optimize.execute(originalQuery)
+
+ // `x > 6` reads no expensive alias, but reading `x` evaluates `a + 1`.
+ val correctAnswer = testStringRelation
+ .where($"b" > 1)
+ .select($"a", expensiveRLike("magic") as "f", $"a" + 1 as "x", $"b")
+ .where($"f" && $"x" > 6)
+ .analyze
+
+ comparePlans(optimized, correctAnswer)
+ }
+
+ test("SPARK-60730: an IS NOT NULL check in front of the condition over an
alias that can " +
+ "fail is not pushed ahead of an expensive one") {
+ // `InferFiltersFromConstraints`, which this suite does not run, puts the
IS NOT NULL
+ // checks it infers in front of the condition, as written here.
+ def project: LogicalPlan = testStringRelation
+ .select($"a", expensiveRLike("magic") as "f", $"a" + 1 as "x", $"a" as
"y", $"b")
+ Seq(true, false).foreach { split =>
+ withSQLConf(SQLConf.SPLIT_PROJECTION_FOR_EXPENSIVE_FILTERS.key ->
split.toString) {
+ // Pushing `isnotnull(x)` would compute `a + 1` ahead of `f`.
+ val originalQuery = project
+ .where(IsNotNull($"x") && $"f" && $"x" > 6 && $"b" > 1)
+ .analyze
+ val Seq(notNull, expensive, failable, _) = splitConjunctivePredicates(
+ originalQuery.collectFirst { case Filter(condition, _) => condition
}.get)
+ val optimized = Optimize.execute(originalQuery)
+ val correctAnswer = testStringRelation
+ .where($"b" > 1)
+ .select($"a", expensiveRLike("magic") as "f", $"a" + 1 as "x", $"a"
as "y", $"b")
+ .where(IsNotNull($"x") && $"f" && $"x" > 6)
+ .analyze
+ comparePlans(optimized, correctAnswer)
+ assertTopFilterConjuncts(optimized, Seq(notNull, expensive, failable))
+
+ // It is pushed when no held condition reads the alias, also when the
first expensive
+ // condition does, which is evaluated first anyway; or when the alias
can not fail.
+ Seq[Expression]($"f", $"f" || $"x" > 6).foreach { first =>
+ val notReadQuery = project
+ .where(IsNotNull($"x") && first && $"a" + 2 > 6)
+ .analyze
+ val notReadAnswer = testStringRelation
+ .where(IsNotNull($"a" + 1))
+ .select($"a", expensiveRLike("magic") as "f", $"a" + 1 as "x",
$"a" as "y", $"b")
+ .where(first && $"a" + 2 > 6)
+ .analyze
+ comparePlans(Optimize.execute(notReadQuery), notReadAnswer)
+ }
+ val plainQuery = project
+ .where(IsNotNull($"y") && $"f" && $"y" + 1 > 6)
+ .analyze
+ val plainAnswer = testStringRelation
+ .where(IsNotNull($"a"))
+ .select($"a", expensiveRLike("magic") as "f", $"a" + 1 as "x", $"a"
as "y", $"b")
+ .where($"f" && $"y" + 1 > 6)
+ .analyze
+ comparePlans(Optimize.execute(plainQuery), plainAnswer)
+ // Only an IS NOT NULL check stays above. A condition written first is
evaluated first
+ // anyway, so it is pushed.
+ val writtenQuery = project
+ .where($"x" > 0 && $"f" && $"x" < 100)
+ .analyze
+ val writtenAnswer = testStringRelation
+ .where($"a" + 1 > 0)
+ .select($"a", expensiveRLike("magic") as "f", $"a" + 1 as "x", $"a"
as "y", $"b")
+ .where($"f" && $"x" < 100)
+ .analyze
+ comparePlans(Optimize.execute(writtenQuery), writtenAnswer)
+ }
+ }
+ // Also after a condition over a Python UDF, which does not hold
conditions back.
+ val pythonUDF = PythonUDF("pythonUDF", null, IntegerType, Seq(attrA),
+ PythonEvalType.SQL_BATCHED_UDF, udfDeterministic = true)
+ val afterPython = testStringRelation
+ .select($"a", pythonUDF.as("p"), expensiveRLike("magic") as "f", $"a" +
1 as "x", $"b")
+ .where($"p" > 1 && IsNotNull($"x") && $"f" && $"x" > 6)
+ .analyze
+ comparePlans(Optimize.execute(afterPython), afterPython)
+ // A throwable condition written after the check stays above with it, in
written order,
+ // while a plain one is still pushed.
+ val raiseError = IsNull(RaiseError(Literal("boom")))
+ val throwableAfter = project
+ .where(IsNotNull($"x") && ($"b" > 1 || raiseError) && $"b" < 10 && $"f"
&& $"x" > 6)
Review Comment:
Added. The `IS NOT NULL` test now checks that a check written in front is
pushed with a division after it in their written order, and the Python UDF test
has `Rand(10) > 0.5` after `p > 1`, which stays above, while `a + 1 > 6` after
it is still pushed, plus the legacy config, which pushes it, a throwable
condition after `p > 1`, and a condition over a Python UDF before or after one
held back. The `throwableAfter` variant and the legacy case with a throwable
alias went away with the kept check.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2642,15 +2650,69 @@ object PushPredicateThroughNonJoin extends
Rule[LogicalPlan] with PredicateHelpe
// eliminate more data quickly and it may also be something which
could be evaluated at
// the storage layer. We may wish to improve this heuristic in the
future.
def isCheap(used: AttributeMap[Alias]): Boolean = !used.exists(a =>
isExpensive(a._2.child))
+ // Whether a component reads an expensive alias that this projection
evaluates. A scalar
+ // Python UDF is not evaluated here: `ExtractPythonUDFs` later moves
it, with its
+ // arguments, to an eval node below the projection, and pushes the
conditions below that
+ // again, except the throwable ones written after one over its output
(SPARK-60633), so
+ // holding a condition back behind one here would gain nothing and
only cost
+ // the pushdown of the batches in between. What is left of the alias
without its Python
+ // UDFs, e.g. a Scala UDF over one, is evaluated here though. Each
Python UDF is replaced
+ // with an attribute, as `ExtractPythonUDFs` does, so that
`isExpensive` sees an input
+ // that is not foldable, as in the projection it leaves.
+ def evaluatedHere(used: AttributeMap[Alias]): Boolean = used.exists {
case (_, alias) =>
+ isExpensive(alias.child) && isExpensive(alias.child.transformDown {
+ case udf if isScalarPythonUDF(udf) =>
AttributeReference("pythonUDF", udf.dataType)()
+ })
+ }
// Split the filter's components into cheap and expensive while
keeping track of
// what each references from the projection. The cheap components
written before the
- // first expensive one are all pushed. After it, a throwable component
(e.g.
- // `raise_error`, possibly inside a projected alias) stays above with
the expensive ones,
- // so it is not evaluated on rows the components before it would have
filtered out.
- val (cheapWithUsed, expensiveWithUsed) = splitPushable(
- originalAndRewrittenConditionsWithUsedAlias)(
- canPush = { case (_, used, _) => isCheap(used) },
- throwable = { case (_, _, replaced) => replaced.throwable })
+ // first expensive one are pushed, except the IS NOT NULL checks kept
above and the
+ // throwable components written after one (see below).
+ // After the first component over an expensive alias the projection
evaluates, which is
+ // the first one held back, only the cheap components that
`canPushAhead` accepts are
+ // pushed; the others stay above with the expensive ones, so they are
only evaluated
+ // where none of the components before them is false. Before that one,
the cheap
+ // components that are not throwable are pushed, as before
SPARK-60730, except the IS NOT
+ // NULL checks kept above.
+ val (written, rest) = originalAndRewrittenConditionsWithUsedAlias.span
{
+ case (_, used, _) => isCheap(used)
+ }
+ val (beforeHeld, fromHeld) = rest.span {
+ case (_, used, _) => isCheap(used) || !evaluatedHere(used)
+ }
+ val (firstHeld, held) = fromHeld.splitAt(1)
Review Comment:
Done. The rule now classifies the components in one loop, in their written
order, and keeps how strictly the cheap ones are checked: none before the first
component kept above, deterministic and not throwable after one only expensive
through Python UDFs, and `canPushAhead` after the first one held back. Only a
component over an expensive alias raises the level: a cheap one kept above,
such as `rand(10) > 0.5` after `p >= 50`, still lets a later deterministic one
be pushed ahead of it, as Spark allows for deterministic conditions, but it
keeps the projection from being split, so the split does not move a later
expensive condition ahead of it. The kept components keep their written order
by construction.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]