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

viirya pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 30cda07393cc [SPARK-59933][SQL] Eliminate quadratic nested field 
aliasing deduplication
30cda07393cc is described below

commit 30cda07393cc6289fdc17a962fcb890a9be3eb42
Author: AnishMahto <[email protected]>
AuthorDate: Mon Oct 5 10:57:42 2026 -0700

    [SPARK-59933][SQL] Eliminate quadratic nested field aliasing deduplication
    
    ### What changes were proposed in this pull request?
    Within any expression, there can be multiple nested struct read 
subexpressions (ex. read `a.b.c`, `a.b`, and `a.d`). All of these are 
independent candidates for column aliasing, and 
`NestedColumnAliasing.getAttributeToExtractValues` is responsible for dropping 
read expressions that are nested anywhere beneath another read. Ex. instead of 
aliasing `a.b.c` and `a.b` independently, we should only alias `a.b` and then 
reuse that alias as `some_alias.c`
    
    But `NestedColumnAliasing` achieves this by iterating over every nested 
read subexpression, and comparing it against every other read subexpression for 
semantic equality on its ancestor.
    
    This is quadratic with respect to the number of nested field read 
subexpressions, which can be noticeable for plans constructed against large 
(wide/deep) tables, especially generated ones. Two behavior-preserving 
optimizations are proposed:
    
    1. Build an `ExpressionSet` of the reads once and look up each parent node 
in it. This is equivalent to `semanticEquals`. `ExpressionSet` stores only 
deterministic expressions in canonical form and canonicalizes the queried 
expression, so a match requires both expressions to be deterministic with equal 
canonical forms.
    2. Plans often repeat the same read many times, so deduplicate with 
`.distinct` before filtering rather than after, to avoid repeated work. The 
result is the same, because removing exact duplicates doesn't change which 
reads are semantically present.
    
    For typical small plans, the overhead is negligible - the previous 
`semanticEquals` comparisons already canonicalized every read, so the only 
added work is building a small hash set.
    
    ### Why are the changes needed?
    Improve nested field read aliasing performance.
    
    ### Does this PR introduce _any_ user-facing change?
    No. This is a pure optimization that should not introduce any behavioral 
change.
    
    ### How was this patch tested?
    No new logic is added, but added tests to bring coverage for existing 
untested contracts (non-deterministic but identically canonicalized plans are 
still aliased separately, and deterministic plans that are canonically the same 
are indeed considered for shared-aliasing).
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Co-authored with Claude Opus 5.5.
    
    Closes #59192 from AnishMahto/SPARK-59933-nested-column-aliasing-dedup.
    
    Authored-by: AnishMahto <[email protected]>
    Signed-off-by: Liang-Chi Hsieh <[email protected]>
---
 .../catalyst/optimizer/NestedColumnAliasing.scala  |  8 ++--
 .../optimizer/NestedColumnAliasingSuite.scala      | 44 ++++++++++++++++++++++
 2 files changed, 48 insertions(+), 4 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/NestedColumnAliasing.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/NestedColumnAliasing.scala
index 8de2663a9809..0f885956f705 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/NestedColumnAliasing.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/NestedColumnAliasing.scala
@@ -275,15 +275,15 @@ object NestedColumnAliasing {
         // Remove redundant [[ExtractValue]]s if they share the same parent 
nest field.
         // For example, when `a.b` and `a.b.c` are in project list, we only 
need to alias `a.b`.
         // Because `a.b` requires all of the inner fields of `b`, we cannot 
prune `a.b.c`.
-        val dedupNestedFields = nestedFields.filter {
+        val distinctNestedFields = nestedFields.distinct
+        val nestedFieldSet = ExpressionSet(distinctNestedFields)
+        val dedupNestedFields = distinctNestedFields.filter {
           // See [[collectExtractValue]]: we only need to deal with 
[[GetArrayStructFields]] and
           // [[GetStructField]]
           case e @ (_: GetStructField | _: GetArrayStructFields) =>
-            val child = e.children.head
-            nestedFields.forall(f => !child.exists(_.semanticEquals(f)))
+            !e.children.head.exists(p => p.deterministic && 
nestedFieldSet.contains(p))
           case _ => true
         }
-          .distinct
           // Discard [[ExtractValue]]s that contain aggregate functions.
           .filterNot(containsAggregateFunction)
 
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/NestedColumnAliasingSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/NestedColumnAliasingSuite.scala
index 93b85470d35d..f2701477cd87 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/NestedColumnAliasingSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/NestedColumnAliasingSuite.scala
@@ -333,6 +333,50 @@ class NestedColumnAliasingSuite extends SchemaPruningTest {
     comparePlans(optimized, expected)
   }
 
+  test("Redundant nested fields are detected through cosmetic variations of 
their parents") {
+    val a = $"a".struct(StructType.fromDDL("b struct<c: int, d: int>, e int"))
+    // `a.b`, referenced many times below.
+    val ab = GetStructField(a, 0, Some("b"))
+    // `a.b` without the field name.
+    val unnamedAb = GetStructField(a, 0, None)
+    // `a.b.c` over the unnamed `a.b`.
+    val abc = GetStructField(unnamedAb, 0, Some("c"))
+    // `a.b.d` over a qualified `a`.
+    val abd = GetStructField(
+      GetStructField(a.withQualifier(Seq("t")), 0, Some("b")), 1, Some("d"))
+
+    val attrToExtractValues = NestedColumnAliasing.getAttributeToExtractValues(
+      Seq.fill(100)(ab) ++ Seq(unnamedAb, abc, abd), Seq.empty)
+
+    assert(attrToExtractValues.keys.map(_.exprId).toSeq == Seq(a.exprId))
+    assert(attrToExtractValues.values.toSeq == Seq(Seq(ab, unnamedAb)))
+  }
+
+  test("Nested fields are not redundant with non-deterministic parents") {
+    // `getAttributeToExtractValues` only returns a column if its kept reads' 
leaf counts sum to
+    // fewer than its own leaf count. The kept reads below sum to 5, so `e` 
through `h` pad `a` to
+    // 6 leaves.
+    val a = $"a".array(StructType.fromDDL("b struct<c: int, d: int>, e int, f 
int, g int, h int"))
+    // `a[ordinal].b` and `a[ordinal].b.c`.
+    def parentAndChild(ordinal: Expression): Seq[ExtractValue] = {
+      val parent = GetStructField(GetArrayItem(a, ordinal), 0, Some("b"))
+      Seq(parent, GetStructField(parent, 0, Some("c")))
+    }
+
+    val deterministicReadPair = parentAndChild(Literal(0))
+    val nondeterministicReadPair = parentAndChild(Cast(Rand(Literal(0L)), 
IntegerType))
+    // `a[rand].b` is non-deterministic, so `a[rand].b.c` is not redundant 
with it, while the
+    // redundant deterministic `a[0].b.c` is still dropped.
+    assert(
+      NestedColumnAliasing.getAttributeToExtractValues(
+        deterministicReadPair ++ nondeterministicReadPair,
+        Seq.empty
+      )
+      .values
+      .toSeq == Seq(deterministicReadPair.take(1) ++ nondeterministicReadPair)
+    )
+  }
+
   test("Nested field pruning for Project and Generate") {
     val query = contact
       .generate(Explode($"friends".getField("first")), outputNames = 
Seq("explode"))


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

Reply via email to