AveryQi115 commented on code in PR #58924:
URL: https://github.com/apache/spark/pull/58924#discussion_r4397544768


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -6869,6 +6869,33 @@ object SQLConf {
       .booleanConf
       .createWithDefault(false)
 
+  val REPLACE_CTE_REF_WITH_CTE_REUSE =
+    buildConf("spark.sql.optimizer.replaceCTERefWithCTEReuse.enabled")
+      .internal()
+      .doc("When true, plan-reuse repartitions (those carrying a nonzero 
repartition id, " +
+        "currently produced only for CTE definitions marked forceSkipInline) 
are converted to " +
+        "CTEReuse nodes instead of plain Repartition nodes; ordinary retained 
or user " +
+        "MATERIALIZED CTEs keep plain repartitions and are unaffected. 
CTEReuse enables " +
+        "guaranteed exchange reuse in AQE by sharing a single inner 
AdaptiveSparkPlanExec " +
+        "across all references.")
+      .version("4.4.0")
+      .withBindingPolicy(ConfigBindingPolicy.SESSION)
+      .booleanConf
+      .createWithDefault(true)
+
+  val FAIL_ON_CTE_REUSE_WITHOUT_AQE =
+    buildConf("spark.sql.optimizer.failOnCTEReuseWithoutAQE.enabled")
+      .internal()
+      .doc("When true, throw an error if guaranteed CTE/subplan shuffle reuse 
fails with AQE " +
+        "off, i.e. two or more shuffles tagged with the same cteId survive 
after " +
+        "ReuseExchangeAndSubquery (they were not deduplicated into a 
ReusedExchangeExec). " +
+        "When false, only log the plan, error, and cteIds. Enabled by default 
in tests to " +
+        "catch reuse regressions.")
+      .version("4.4.0")
+      .withBindingPolicy(ConfigBindingPolicy.SESSION)
+      .booleanConf
+      .createWithDefault(false)

Review Comment:
   Thanks for the detailed suggestion — I'd like to keep the current verifier 
for this PR; I think it's actually the safer choice here. A rule that 
force-rewrites every reference onto one owner by `cteId` is risky exactly when 
the references' canonical bodies aren't identical: it would wire 
structurally-different bodies onto a single materialization and could silently 
return wrong rows. The current path only seals when the bodies are canonically 
equal (via the normal `ReuseExchangeAndSubquery` dedup); if duplicates survive 
it surfaces that rather than forcing a merge — throwing when 
`failOnCTEReuseWithoutAQE` is on (so tests catch regressions) and logging the 
plan + cteIds and continuing when it's off, which preserves the existing 
behavior. Today the only producer of `forceSkipInline` CTEs is the DSv2 MERGE 
path, and this matches that use case. I'd suggest revisiting a stronger 
cteId-keyed owner (with canonical-body validation) once more producers rely on 
guaranteed reuse.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/SparkStrategies.scala:
##########
@@ -1196,6 +1196,34 @@ abstract class SparkStrategies extends 
QueryPlanner[SparkPlan] {
           f, key, lObj, rObj, lGroup, rGroup, lAttr, rAttr, lOrder, rOrder, 
oAttr,
           planLater(left), planLater(right)) :: Nil
 
+      case r: logical.CTEReuseRelation =>
+        // Plan the shared subplan eagerly rather than using planLater, because
+        // CTEReuseExchange is a LeafExecNode whose subplan is not visible via
+        // children. QueryPlanner.collectPlaceholders would never find a 
PlanLater
+        // inside the subplan, leaving it unresolved.
+        //
+        // sharedSubplan is a RepartitionByExpression wrapping the CTE body. 
Planning it produces a
+        // plain ShuffleExchangeExec; we re-stamp its origin as 
LOCAL_SHUFFLE_FOR_CTE(cteId) so the
+        // shuffle is recognized as a reuse boundary 
(`isCreatedForSubplanReuse`) by
+        // EnsureRequirements, the AQE-off unwrap path, and VerifyCTEReuse. We 
use this shuffle
+        // directly instead of adding a separate exchange -- the Repartition 
serves as the logical
+        // plan for the inner AQE, enabling correct re-optimization (broadcast 
stages are findable
+        // as descendants, and re-planning preserves the root exchange).
+        val physicalSubplan = SparkStrategies.this.plan(r.sharedSubplan).next()
+        val shuffle = physicalSubplan match {
+          case s: ShuffleExchangeExec => s.copy(shuffleOrigin = 
LOCAL_SHUFFLE_FOR_CTE(r.cteId))

Review Comment:
   Good point on the repeated planning cost — I'd prefer to leave this as a 
follow-up rather than fold it into this PR. The current behavior matches how 
exchange/subquery reuse already works: each reference is planned independently 
and shared afterward, not memoized during planning. A per-cteId planning memo 
would also have to account for the AQE-off unwrap path — once 
`CTEReuseExchange` is unwrapped, the shared subplan can need its exprIds 
re-assigned per reference, so a single immutable physical template isn't a 
drop-in. It's a worthwhile optimization, but orthogonal to the correctness of 
this change, so I'd rather do it separately.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -6869,6 +6869,33 @@ object SQLConf {
       .booleanConf
       .createWithDefault(false)
 
+  val REPLACE_CTE_REF_WITH_CTE_REUSE =
+    buildConf("spark.sql.optimizer.replaceCTERefWithCTEReuse.enabled")
+      .internal()
+      .doc("When true, plan-reuse repartitions (those carrying a nonzero 
repartition id, " +
+        "currently produced only for CTE definitions marked forceSkipInline) 
are converted to " +
+        "CTEReuse nodes instead of plain Repartition nodes; ordinary retained 
or user " +
+        "MATERIALIZED CTEs keep plain repartitions and are unaffected. 
CTEReuse enables " +
+        "guaranteed exchange reuse in AQE by sharing a single inner 
AdaptiveSparkPlanExec " +
+        "across all references.")
+      .version("4.4.0")
+      .withBindingPolicy(ConfigBindingPolicy.SESSION)
+      .booleanConf
+      .createWithDefault(true)

Review Comment:
   The default-on is intentional — Julek asked for it so the new lowering gets 
broad coverage. It only changes plans for `forceSkipInline` CTEs (today just 
the DSv2 MERGE path), so other plans are unaffected even with it on. You're 
right the description is inconsistent, though — I'll update the PR text so the 
"defaults off" wording matches the actual default, and drop the 
`LocalPartition`/Photon references that aren't in Apache Spark.



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

Reply via email to