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

cloud-fan pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 388c247d0aef [SPARK-58102][SQL] Fix SLAM scope extraction bailing out 
on ReusedSubqueryExec
388c247d0aef is described below

commit 388c247d0aefed39ac383c3c9ff681e30573a65a
Author: Juliusz Sompolski <[email protected]>
AuthorDate: Wed Jul 15 12:32:41 2026 +0800

    [SPARK-58102][SQL] Fix SLAM scope extraction bailing out on 
ReusedSubqueryExec
    
    ### What changes were proposed in this pull request?
    
    `SQLLastAttemptAccumulator.extractStageRDDScopes` walks a Dataset's 
`executedPlan` to find the
    `RDDOperationScope` ids of the stages that make up its execution, so that 
`SQLLastAttemptAccumulator`
    can attribute "last attempt" metric updates to that execution. Its 
`BaseSubqueryExec` match handles
    `SubqueryExec`, `SubqueryBroadcastExec` and 
`SubqueryAdaptiveBroadcastExec`, and then bails out on a
    catch-all `case p =>` that records a `bailOutReason`. When extraction bails 
out,
    `lastAttemptValueForQueryExecution` / `lastAttemptValueForDataset` return 
`None`.
    
    `ReusedSubqueryExec` was not enumerated, so any plan containing a reused 
subquery hit the catch-all
    and caused `lastAttemptValueForDataset` / 
`lastAttemptValueForQueryExecution` to return `None`.
    Subquery reuse is enabled by default (`spark.sql.execution.reuseSubquery`), 
and a
    `ReusedSubqueryExec` is produced whenever the same subquery appears more 
than once in a plan --
    including when predicate pushdown duplicates a subquery into a scan's data 
filters.
    
    This PR adds a `ReusedSubqueryExec` case that recurses into its `child`, 
mirroring the existing
    `ReusedExchangeExec` handling. `ReusedSubqueryExec` is a `LeafExecNode` 
whose `child` is a field
    rather than a plan child, so the normal plan traversal never descends into 
it; recursing collects
    the reused subquery's scopes. If the original `SubqueryExec` is still 
present elsewhere in the plan,
    its scopes are collected twice, but that is harmless: 
`lastAttemptValueForRDDScopes` deduplicates by
    scope (it reduces the collected scope ids to a set and aggregates one RDD 
per scope).
    
    ### Why are the changes needed?
    
    Without this, `lastAttemptValueForDataset` / 
`lastAttemptValueForQueryExecution` silently return
    `None` for any query whose physical plan contains a `ReusedSubqueryExec`, 
even though the metric was
    tracked correctly. Because subquery reuse is on by default, this affects 
common queries -- e.g. the
    same scalar/`IN` subquery referenced twice, or a subquery duplicated into a 
`FileScan`'s data filters
    by predicate pushdown.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Added a test to `SQLLastAttemptMetricPlanShapesSuite` whose physical plan 
contains a
    `ReusedSubqueryExec`: a scalar subquery with a nested `(SELECT MIN(id) FROM 
range(5))` predicate that
    gets pushed into the outer subquery's scan data filters and duplicated, so 
subquery reuse rewrites one
    copy to a `ReusedSubqueryExec`. The test asserts both the metric value and 
(via
    `testPhysicalPlanShape`) that a `ReusedSubqueryExec` is actually present in 
the plan -- with a
    fallback for the forced-AQE-replan variants, which bypass subquery reuse 
and keep the duplicated
    subquery as two separate `SubqueryExec` nodes (this mirrors the existing 
`exchange - Shuffle` test's
    `ReusedExchangeExec` fallback). Before the fix, 
`lastAttemptValueForDataset` returned `None` for this
    plan (assertion `None did not equal Some(300)`); after the fix it returns 
the expected value. The
    test runs across the suite's existing `useAQE` x `failureMode` x 
`aqeReplans` matrix (9 variants).
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8)
    
    Closes #57226 from juliuszsompolski/SPARK-58102-reused-subquery-slam.
    
    Authored-by: Juliusz Sompolski <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
    (cherry picked from commit 53d48e3cc4bbc301003100fcde06564744b47c1b)
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../metric/SQLLastAttemptAccumulator.scala         |  6 ++++-
 .../SQLLastAttemptMetricPlanShapesSuite.scala      | 27 +++++++++++++++++++++-
 2 files changed, 31 insertions(+), 2 deletions(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/metric/SQLLastAttemptAccumulator.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/metric/SQLLastAttemptAccumulator.scala
index 114d3974bb0e..245e5204104c 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/metric/SQLLastAttemptAccumulator.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/metric/SQLLastAttemptAccumulator.scala
@@ -23,7 +23,7 @@ import scala.util.control.NonFatal
 import org.apache.spark.SparkContext
 import org.apache.spark.internal.{LogEntry, Logging}
 import org.apache.spark.sql.Dataset
-import org.apache.spark.sql.execution.{BaseSubqueryExec, QueryExecution, 
SparkPlan, SubqueryAdaptiveBroadcastExec, SubqueryBroadcastExec, SubqueryExec, 
WholeStageCodegenExec}
+import org.apache.spark.sql.execution.{BaseSubqueryExec, QueryExecution, 
ReusedSubqueryExec, SparkPlan, SubqueryAdaptiveBroadcastExec, 
SubqueryBroadcastExec, SubqueryExec, WholeStageCodegenExec}
 import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
 import org.apache.spark.sql.execution.exchange.{BroadcastExchangeExec, 
BroadcastExchangeLike, ReusedExchangeExec, ShuffleExchangeExec, 
ShuffleExchangeLike}
 import org.apache.spark.util.{AccumulatorV2, LastAttemptAccumulator}
@@ -374,6 +374,10 @@ object SQLLastAttemptAccumulator extends Logging {
         case _: SubqueryAdaptiveBroadcastExec =>
           // Used by DPP filter only.
           Nil
+        case r: ReusedSubqueryExec =>
+          // Reused subquery is going to reuse stuff executed in the scope of 
its child,
+          // i.e. the subquery it reuses.
+          recurse(r.child)
         case p =>
           // Bail out if future unknown implementation is encountered.
           bailOutReason = Some(s"Unsupported BaseSubqueryExec: 
${p.getClass.getName}")
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/metric/SQLLastAttemptMetricPlanShapesSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/metric/SQLLastAttemptMetricPlanShapesSuite.scala
index d51c8cae1caa..033a0eea5b5a 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/metric/SQLLastAttemptMetricPlanShapesSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/metric/SQLLastAttemptMetricPlanShapesSuite.scala
@@ -22,7 +22,7 @@ import scala.util.Random
 import org.scalatest.Tag
 
 import org.apache.spark.internal.config
-import org.apache.spark.sql.execution.{CollectLimitExec, RDDScanExec, 
SparkPlan}
+import org.apache.spark.sql.execution.{CollectLimitExec, RDDScanExec, 
ReusedSubqueryExec, SparkPlan, SubqueryExec}
 import org.apache.spark.sql.execution.adaptive.{AdaptiveSparkPlanHelper, 
AQETestHelper, DisableAdaptiveExecutionSuite}
 import org.apache.spark.sql.execution.columnar.InMemoryTableScanExec
 import org.apache.spark.sql.execution.exchange._
@@ -298,6 +298,31 @@ class SQLLastAttemptMetricPlanShapesSuite
     metricValueCheck = MetricValue.exactly(NUM_RECORDS)
   )
 
+  testPhysicalPlanShape(
+    // The inner `(SELECT MIN(id) FROM range(5))` subquery gets pushed into 
the outer subquery's
+    // scan DataFilters and duplicated, so subquery reuse turns one copy into a
+    // ReusedSubqueryExec. extractStageRDDScopes must handle that node instead 
of bailing out
+    // (which would make lastAttemptValueForDataset return None for the whole 
plan).
+    label = "subquery - scalar - reused",
+    sqlQuery =
+      s"""SELECT *
+         | FROM $TABLE_NAME
+         | WHERE id == (
+         |   SELECT MAX(low_cardinality_col)
+         |   FROM $TABLE_NAME
+         |   WHERE increment_metric()
+         |     AND low_cardinality_col >= (
+         |       SELECT MIN(id)
+         |       FROM range(5)))""".stripMargin,
+    metricValueCheck = MetricValue.exactly(NUM_RECORDS),
+    executedPlanCheck = PhysicalPlan.exists {
+      case _: ReusedSubqueryExec => true
+      // Forced AQE replans bypass subquery reuse, so the duplicated inner 
subquery stays as two
+      // separate SubqueryExec nodes instead of a ReusedSubqueryExec.
+      case _: SubqueryExec if PhysicalPlan.hasAQEReplans => true
+    }
+  )()
+
   testPhysicalPlanShape(
     label = "subquery - EXISTS",
     sqlQuery =


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

Reply via email to