peter-toth commented on code in PR #58293:
URL: https://github.com/apache/spark/pull/58293#discussion_r3860906211


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/columnar/InMemoryRelation.scala:
##########
@@ -713,13 +713,11 @@ case class InMemoryRelation(
     InMemoryRelation(newOutput, cacheBuilder, newOutputOrdering, 
statsOfPlanToCache)
   }
 
-  override def newInstance(): this.type = {
-    InMemoryRelation(
-      output.map(_.newInstance()),
-      cacheBuilder,
-      outputOrdering,
-      statsOfPlanToCache).asInstanceOf[this.type]
-  }
+  // Goes through `withOutput` so that `outputOrdering` is re-mapped onto the 
fresh exprIds.
+  // Returning a relation whose `outputOrdering` still references the old 
attributes would break
+  // canonicalization, which re-maps the ordering through the relation's own 
`output`.
+  override def newInstance(): this.type =
+    withOutput(output.map(_.newInstance())).asInstanceOf[this.type]

Review Comment:
   **Finding 1.** `withOutput` re-maps `outputOrdering` but hands 
`statsOfPlanToCache` straight through, so `newInstance()` still returns a 
relation whose `Statistics.attributeStats` is keyed by the *old* attributes. 
`FilterEstimation` and `JoinEstimation` look those up by the plan's own 
attributes, so every column stat is silently dropped for the deduplicated copy.
   
   Measured on this commit, CBO on, over a 1000-row analyzed table cached with 
an ordering:
   
   ```
   ### relation stats (orig): sizeInBytes=15.6 KiB, rowCount=1.00E+3
   ### relation stats (new) : sizeInBytes=15.6 KiB, rowCount=1.00E+3
   ### Filter(id > 900) over ORIGINAL : sizeInBytes=1600.0 B, rowCount=100
   ### Filter(id > 900) over NEW COPY : sizeInBytes=15.6 KiB, rowCount=1.00E+3
   ```
   
   A 10x overestimate on the branch that went through `newInstance()`, which is 
enough to flip a join strategy or a build side. No error, just a worse plan.
   
   The sibling `MultiInstanceRelation` gets this right: 
`LogicalRDD.newInstance()` 
(`sql/core/src/main/scala/org/apache/spark/sql/execution/ExistingRDD.scala:119`)
 re-maps partitioning, ordering *and* stats, and its helper is `private[sql]` 
and already in scope here. In `withOutput`:
   
   ```scala
   val newStats = if (statsOfPlanToCache == null) {
     null
   } else {
     LogicalRDD.rewriteStatistics(statsOfPlanToCache, map)
   }
   InMemoryRelation(newOutput, cacheBuilder, newOutputOrdering, newStats)
   ```
   
   I applied that: the lookup returns `Some(ColumnStat(...))`, the filter 
estimate goes back to 100 rows, and `CachedTableSuite` + 
`InMemoryRelationSuite` + `DatasetCacheSuite` stay green. The null guard is 
needed because `statsOfPlanToCache` is a `var` initialized to `null`.
   
   Pre-existing and CBO-only, so a follow-up ticket is fine if you'd rather 
keep this PR to the crash.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/columnar/InMemoryRelationSuite.scala:
##########
@@ -34,6 +35,20 @@ class InMemoryRelationSuite extends SparkFunSuite
     assert(r1.sameResult(r2))
   }
 
+  test("SPARK-59009: newInstance() re-maps outputOrdering onto the new 
attributes") {
+    val d = spark.range(10).selectExpr("id", "id % 3 AS k").orderBy("k", "id")
+    val r1 = InMemoryRelation(StorageLevel.MEMORY_ONLY, d.queryExecution, None)
+    assert(r1.outputOrdering.nonEmpty)
+
+    val r2 = r1.newInstance()
+    assert(r2.output.map(_.exprId) != r1.output.map(_.exprId))
+    // The ordering must be re-mapped onto the new attributes, not left 
referencing the old ones.
+    assert(r2.outputOrdering.nonEmpty)
+    
assert(AttributeSet(r2.outputOrdering.flatMap(_.references)).subsetOf(AttributeSet(r2.output)))
+    // Canonicalization re-maps `outputOrdering` through `output`, so a stale 
ordering would throw.
+    r2.canonicalized

Review Comment:
   **Finding 2.** A bare `r2.canonicalized` only pins "doesn't throw". 
`sameResult` is the property that actually matters for a 
`MultiInstanceRelation` (it is what `CacheManager` lookups and exchange reuse 
rely on), it still fails on base because it throws inside `doCanonicalize`, and 
it mirrors the SPARK-46779 test right above.
   
   ```suggestion
       assert(r1.sameResult(r2))
   ```
   
   I checked `r1.sameResult(r2)` is `true` with the fix in place.
   



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