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

XiDuo You updated SPARK-61211:
------------------------------
    Description: 
UnionExec.prepareOutputPartitioning can report an incorrect hash layout when a 
child output repeats an exprId. Runtime BindReferences selects the first 
occurrence, while AttributeMap(child.output.zip(union.output)) retains the last 
occurrence. A union can consequently advertise clustering on a column whose 
rows were actually shuffled by another column, causing EnsureRequirements to 
omit a necessary exchange.

This is independent of SPARK-60379's shuffle push-down. Reviewers reproduced 
the existing bug on merge base 352ac9bf228 with push-down off, and with AQE 
both on and off. Disabling spark.sql.unionOutputPartitioning restores the 
expected result. The union output partitioning mapping dates back to 
SPARK-52921 and shipped in 4.1.0; the two analyzer confs this shape needs were 
also added in 4.1.0, so 4.1.0 is the earliest affected release.

Reported reproduction (Scala):
{code:scala}
import org.apache.spark.sql.functions._
import org.apache.spark.sql.expressions.Window
import spark.implicits._

spark.conf.set("spark.sql.analyzer.unionIsResolvedWhenDuplicatesPerChildResolved",
 "false")
spark.conf.set("spark.sql.enforceTypeCoercionBeforeUnionDeduplication.enabled", 
"false")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "false")
spark.conf.set("spark.sql.shuffle.partitions", "5")
spark.conf.set("spark.sql.unionOutputPartitioning", "true")
// Test harness controls; avoid resolver dual-run validation and codegen 
compilation of duplicates.
spark.conf.set("spark.sql.analyzer.singlePassResolver.dualRunWithLegacy", 
"false")
spark.conf.set("spark.sql.codegen.wholeStage", "false")

val p1 = spark.range(1000).selectExpr("id p", "id w").select($"p", $"p", $"w")
val p2 = spark.range(1000, 11000).selectExpr("id q", "id % 1000 r", "id w2")
val x = spark.range(1000).selectExpr("id x", "id c", "id u").repartition($"c")
val r = p1.union(p2).repartition(p1.col("p"))
val result = x.union(r).withColumn("rn",
  row_number().over(Window.partitionBy(x.col("c")).orderBy(x.col("u"))))
result.collect().count(_.getInt(3) == 1)
{code}

Expected: 1000. Reported actual: 4582. Collect all columns before counting to 
retain the triggering plan shape.

The duplicate second child reports hash(p), physically bound to its first 
column. Its mapping into the outer union reports hash(c), corresponding to the 
second column. Both outer children then appear to report hash(c), although 
their rows are not co-located by c.

Investigate a conservative refusal to propagate partitioning from ambiguous 
child outputs, or a mapping aligned with first-occurrence runtime binding, with 
coverage for both mapping directions and all supported partitioning types. Keep 
this general UnionExec fix separate from the narrower push-down eligibility 
guards in SPARK-60379.

Reviewer evidence:
https://github.com/apache/spark/pull/59630#discussion_r4495483202
https://github.com/apache/spark/pull/59630#discussion_r4493424741


  was:
UnionExec.prepareOutputPartitioning can report an incorrect hash layout when a 
child output repeats an exprId. Runtime BindReferences selects the first 
occurrence, while AttributeMap(child.output.zip(union.output)) retains the last 
occurrence. A union can consequently advertise clustering on a column whose 
rows were actually shuffled by another column, causing EnsureRequirements to 
omit a necessary exchange.

This is independent of SPARK-60379's shuffle push-down. Reviewers reproduced 
the existing bug on merge base 352ac9bf228 with push-down off, and with AQE 
both on and off. Disabling spark.sql.unionOutputPartitioning restores the 
expected result. The exact earliest affected release has not yet been 
determined.

Reported reproduction (Scala):
{code:scala}
import org.apache.spark.sql.functions._
import org.apache.spark.sql.expressions.Window
import spark.implicits._

spark.conf.set("spark.sql.analyzer.unionIsResolvedWhenDuplicatesPerChildResolved",
 "false")
spark.conf.set("spark.sql.enforceTypeCoercionBeforeUnionDeduplication.enabled", 
"false")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "false")
spark.conf.set("spark.sql.shuffle.partitions", "5")
spark.conf.set("spark.sql.unionOutputPartitioning", "true")
// Test harness controls; avoid resolver dual-run validation and codegen 
compilation of duplicates.
spark.conf.set("spark.sql.analyzer.singlePassResolver.dualRunWithLegacy", 
"false")
spark.conf.set("spark.sql.codegen.wholeStage", "false")

val p1 = spark.range(1000).selectExpr("id p", "id w").select($"p", $"p", $"w")
val p2 = spark.range(1000, 11000).selectExpr("id q", "id % 1000 r", "id w2")
val x = spark.range(1000).selectExpr("id x", "id c", "id u").repartition($"c")
val r = p1.union(p2).repartition(p1.col("p"))
val result = x.union(r).withColumn("rn",
  row_number().over(Window.partitionBy(x.col("c")).orderBy(x.col("u"))))
result.collect().count(_.getInt(3) == 1)
{code}

Expected: 1000. Reported actual: 4582. Collect all columns before counting to 
retain the triggering plan shape.

The duplicate second child reports hash(p), physically bound to its first 
column. Its mapping into the outer union reports hash(c), corresponding to the 
second column. Both outer children then appear to report hash(c), although 
their rows are not co-located by c.

Investigate a conservative refusal to propagate partitioning from ambiguous 
child outputs, or a mapping aligned with first-occurrence runtime binding, with 
coverage for both mapping directions and all supported partitioning types. Keep 
this general UnionExec fix separate from the narrower push-down eligibility 
guards in SPARK-60379.

Reviewer evidence:
https://github.com/apache/spark/pull/59630#discussion_r4495483202
https://github.com/apache/spark/pull/59630#discussion_r4493424741



> Avoid incorrect union partitioning with duplicate child expression IDs
> ----------------------------------------------------------------------
>
>                 Key: SPARK-61211
>                 URL: https://issues.apache.org/jira/browse/SPARK-61211
>             Project: Spark
>          Issue Type: Bug
>          Components: SQL
>    Affects Versions: 4.1.0, 4.2.0, 4.3.0, 5.0.0, 4.4.0
>            Reporter: XiDuo You
>            Priority: Major
>
> UnionExec.prepareOutputPartitioning can report an incorrect hash layout when 
> a child output repeats an exprId. Runtime BindReferences selects the first 
> occurrence, while AttributeMap(child.output.zip(union.output)) retains the 
> last occurrence. A union can consequently advertise clustering on a column 
> whose rows were actually shuffled by another column, causing 
> EnsureRequirements to omit a necessary exchange.
> This is independent of SPARK-60379's shuffle push-down. Reviewers reproduced 
> the existing bug on merge base 352ac9bf228 with push-down off, and with AQE 
> both on and off. Disabling spark.sql.unionOutputPartitioning restores the 
> expected result. The union output partitioning mapping dates back to 
> SPARK-52921 and shipped in 4.1.0; the two analyzer confs this shape needs 
> were also added in 4.1.0, so 4.1.0 is the earliest affected release.
> Reported reproduction (Scala):
> {code:scala}
> import org.apache.spark.sql.functions._
> import org.apache.spark.sql.expressions.Window
> import spark.implicits._
> spark.conf.set("spark.sql.analyzer.unionIsResolvedWhenDuplicatesPerChildResolved",
>  "false")
> spark.conf.set("spark.sql.enforceTypeCoercionBeforeUnionDeduplication.enabled",
>  "false")
> spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "false")
> spark.conf.set("spark.sql.shuffle.partitions", "5")
> spark.conf.set("spark.sql.unionOutputPartitioning", "true")
> // Test harness controls; avoid resolver dual-run validation and codegen 
> compilation of duplicates.
> spark.conf.set("spark.sql.analyzer.singlePassResolver.dualRunWithLegacy", 
> "false")
> spark.conf.set("spark.sql.codegen.wholeStage", "false")
> val p1 = spark.range(1000).selectExpr("id p", "id w").select($"p", $"p", $"w")
> val p2 = spark.range(1000, 11000).selectExpr("id q", "id % 1000 r", "id w2")
> val x = spark.range(1000).selectExpr("id x", "id c", "id u").repartition($"c")
> val r = p1.union(p2).repartition(p1.col("p"))
> val result = x.union(r).withColumn("rn",
>   row_number().over(Window.partitionBy(x.col("c")).orderBy(x.col("u"))))
> result.collect().count(_.getInt(3) == 1)
> {code}
> Expected: 1000. Reported actual: 4582. Collect all columns before counting to 
> retain the triggering plan shape.
> The duplicate second child reports hash(p), physically bound to its first 
> column. Its mapping into the outer union reports hash(c), corresponding to 
> the second column. Both outer children then appear to report hash(c), 
> although their rows are not co-located by c.
> Investigate a conservative refusal to propagate partitioning from ambiguous 
> child outputs, or a mapping aligned with first-occurrence runtime binding, 
> with coverage for both mapping directions and all supported partitioning 
> types. Keep this general UnionExec fix separate from the narrower push-down 
> eligibility guards in SPARK-60379.
> Reviewer evidence:
> https://github.com/apache/spark/pull/59630#discussion_r4495483202
> https://github.com/apache/spark/pull/59630#discussion_r4493424741



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

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

Reply via email to