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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4d9468cdba3 [fix](subquery) Resolve correlated qualified columns 
before dereference (#67438)
4d9468cdba3 is described below

commit 4d9468cdba36a227f071ad9d7a56fcc8c03b8f16
Author: Calvin Kirs <[email protected]>
AuthorDate: Mon Sep 7 10:47:28 2026 +0800

    [fix](subquery) Resolve correlated qualified columns before dereference 
(#67438)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    In a correlated subquery, a multipart reference can represent either a
    relation-qualified column (`table_alias.column`) or a nested-field
    dereference (`column.field`). Nereids previously searched the inner
    scope completely before checking the outer scope. If an inner table had
    a physical column with the same name as an outer table alias, the outer
    reference could therefore be interpreted as a nested field of that inner
    column.
    
    For scalar inner columns this raised a `No such field` analysis error.
    For complex inner columns it could bind successfully to the wrong
    expression and produce incorrect results.
    
    #### Example
    
    ```sql
    CREATE TABLE outer_events (
        id INT,
        `@event_name` VARCHAR(32)
    )
    DISTRIBUTED BY HASH(id) BUCKETS 1
    PROPERTIES ("replication_num" = "1");
    
    CREATE TABLE inner_events (
        id INT,
        t1 INT
    )
    DISTRIBUTED BY HASH(id) BUCKETS 1
    PROPERTIES ("replication_num" = "1");
    
    INSERT INTO outer_events VALUES (1, 'blocked'), (2, 'kept');
    INSERT INTO inner_events VALUES (1, 0);
    
    SELECT t1.id, t1.`@event_name`
    FROM outer_events t1
    WHERE NOT EXISTS (
        SELECT 1
        FROM inner_events inner_alias
        WHERE t1.`@event_name` = 'blocked'
    )
    ORDER BY t1.id;
    ```
    
    Before this fix, the outer alias `t1` conflicted with the physical inner
    column `inner_events.t1`. Nereids treated ``t1.`@event_name``` as a
    nested-field access on the inner scalar column and failed during
    analysis:
    
    ```text
    No such field '@event_name' in 't1'
    ```
    
    After this fix, ``t1.`@event_name``` is correctly bound to the outer
    relation alias and the query returns:
    
    ```text
    +------+-------------+
    | id   | @event_name |
    +------+-------------+
    |    2 | kept        |
    +------+-------------+
    ```
    
    This change resolves relation-qualified columns in the current and outer
    scopes before falling back to first-part-as-column dereference. The
    relation-only phase follows each analyzer's complete local scope order
    before searching the outer scope, so custom HAVING and QUALIFY scopes
    preserve normal nearest-relation shadowing.
    
    Lambda lexical scope is preserved: in `array_map(x -> x.value,
    x.items)`, the first `x` inside the lambda resolves to the lambda
    argument while the second `x` resolves to the enclosing table alias.
    When an outer qualified reference contains nested fields, such as
    `outer_alias.payload.k`, the underlying `payload` slot is also recorded
    as a correlated slot.
    
    ### Release note
    
    Fix incorrect Nereids column binding when an outer table alias conflicts
    with an inner column name in a correlated subquery.
    
    ### Check List (For Author)
    
    - Test: Regression test / Unit Test
    - `./run-fe-ut.sh --run
    org.apache.doris.nereids.rules.analysis.TestDereference`
        - Result: 10 tests passed, 0 failures
    - Added `query_p0/test_dereference` regression coverage for scalar and
    complex inner columns, lambda lexical scope, outer nested-field
    correlation, reused inner/outer aliases in HAVING and QUALIFY, and local
    qualifier shadowing
        - `./run-regression-test.sh --run -d query_p0 -s test_dereference`
        - Result: suite passed against the generated snapshot
    - Behavior changed: Yes (relation-qualified correlated references now
    take priority over inner column-field dereference while preserving
    complete local-scope precedence)
    - Does this need documentation: No
---
 .../org/apache/doris/nereids/analyzer/Scope.java   |  19 ++
 .../nereids/rules/analysis/BindExpression.java     | 158 ++++++++++------
 .../nereids/rules/analysis/ExpressionAnalyzer.java | 203 +++++++++++++++++++--
 .../apache/doris/nereids/analyzer/ScopeTest.java   |  48 +++++
 .../rules/analysis/ExpressionAnalyzerTest.java     |  51 ++++++
 .../nereids/rules/analysis/TestDereference.java    | 135 ++++++++++++++
 regression-test/data/query_p0/test_dereference.out |  40 ++++
 .../suites/query_p0/test_dereference.groovy        | 168 ++++++++++++++++-
 8 files changed, 752 insertions(+), 70 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/Scope.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/Scope.java
index cb74698a62b..58590e74f02 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/Scope.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/Scope.java
@@ -21,6 +21,7 @@ import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.util.Utils;
 
 import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
@@ -66,6 +67,7 @@ public class Scope {
     private final boolean buildNameToSlot;
     private final Supplier<ListMultimap<String, Slot>> nameToSlot;
     private final Supplier<ListMultimap<String, Slot>> nameToAsteriskSlot;
+    private final Supplier<ImmutableSetMultimap<String, List<String>>> 
relationNameToQualifiers;
 
     public Scope(List<Slot> slots) {
         this(Optional.empty(), slots);
@@ -87,6 +89,7 @@ public class Scope {
         this.buildNameToSlot = slots.size() > 500;
         this.nameToSlot = buildNameToSlot ? 
Suppliers.memoize(this::buildNameToSlot) : null;
         this.nameToAsteriskSlot = buildNameToSlot ? 
Suppliers.memoize(this::buildNameToAsteriskSlot) : null;
+        this.relationNameToQualifiers = 
Suppliers.memoize(this::buildRelationNameToQualifiers);
         this.asteriskSlots = Utils.fastToImmutableList(
                 Objects.requireNonNull(asteriskSlots, "asteriskSlots can not 
be null"));
     }
@@ -107,6 +110,11 @@ public class Scope {
         return correlatedSlots;
     }
 
+    /** Find distinct relation qualifiers by relation name, ignoring case. */
+    public Set<List<String>> findRelationQualifiersIgnoreCase(String 
relationName) {
+        return 
relationNameToQualifiers.get().get(relationName.toUpperCase(Locale.ROOT));
+    }
+
     /** findSlotIgnoreCase */
     public List<Slot> findSlotIgnoreCase(String slotName, boolean all) {
         List<Slot> slots = all ? this.slots : this.asteriskSlots;
@@ -140,4 +148,15 @@ public class Scope {
         }
         return map;
     }
+
+    private ImmutableSetMultimap<String, List<String>> 
buildRelationNameToQualifiers() {
+        ImmutableSetMultimap.Builder<String, List<String>> builder = 
ImmutableSetMultimap.builder();
+        for (Slot slot : slots) {
+            if (!slot.getQualifier().isEmpty()) {
+                List<String> qualifier = slot.getQualifier();
+                builder.put(qualifier.get(qualifier.size() - 
1).toUpperCase(Locale.ROOT), qualifier);
+            }
+        }
+        return builder.build();
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
index 74019610b15..3ee5312f4b0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
@@ -625,7 +625,8 @@ public class BindExpression implements AnalysisRuleFactory {
         Supplier<CustomSlotBinderAnalyzer> bindByAggChild = 
Suppliers.memoize(() -> {
             Scope aggChildOutputScope
                     = toScope(cascadesContext, 
PlanUtils.fastGetChildrenOutputs(aggregate.children()));
-            return (analyzer, unboundSlot) -> 
analyzer.bindSlotByScope(unboundSlot, aggChildOutputScope);
+            return (analyzer, unboundSlot, bindRelationQualifierOnly) ->
+                    analyzer.bindSlotByScope(unboundSlot, aggChildOutputScope, 
bindRelationQualifierOnly);
         });
 
         Scope aggOutputScope = toScope(cascadesContext, aggregate.getOutput());
@@ -640,19 +641,23 @@ public class BindExpression implements 
AnalysisRuleFactory {
             }
             Scope groupBySlotsScope = toScope(cascadesContext, 
groupBySlots.build());
 
-            return (analyzer, unboundSlot) -> {
-                List<Expression> boundInGroupBy = 
analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope);
-                if (!boundInGroupBy.isEmpty()) {
-                    return ImmutableList.of(boundInGroupBy.get(0));
+            return (analyzer, unboundSlot, bindRelationQualifierOnly) -> {
+                ExpressionAnalyzer.SlotBinding boundInGroupBy = 
analyzer.bindSlotByScope(
+                        unboundSlot, groupBySlotsScope, 
bindRelationQualifierOnly);
+                if (!boundInGroupBy.getBoundSlots().isEmpty()) {
+                    return boundInGroupBy.firstOrEmpty();
                 }
 
-                List<Expression> boundInAggOutput = 
analyzer.bindSlotByScope(unboundSlot, aggOutputScope);
-                if (!boundInAggOutput.isEmpty()) {
-                    return ImmutableList.of(boundInAggOutput.get(0));
+                ExpressionAnalyzer.SlotBinding boundInAggOutput = 
analyzer.bindSlotByScope(
+                        unboundSlot, aggOutputScope, 
bindRelationQualifierOnly);
+                if (!boundInAggOutput.getBoundSlots().isEmpty()) {
+                    return 
boundInAggOutput.firstOrEmpty().withQualifierOccupancyFrom(boundInGroupBy);
                 }
 
-                List<? extends Expression> expressions = 
bindByAggChild.get().bindSlot(analyzer, unboundSlot);
-                return expressions.isEmpty() ? expressions : 
ImmutableList.of(expressions.get(0));
+                return bindByAggChild.get().bindSlot(analyzer, unboundSlot, 
bindRelationQualifierOnly)
+                        .firstOrEmpty()
+                        .withQualifierOccupancyFrom(boundInGroupBy)
+                        .withQualifierOccupancyFrom(boundInAggOutput);
             };
         });
 
@@ -693,9 +698,19 @@ public class BindExpression implements AnalysisRuleFactory 
{
             @Override
             protected List<? extends Expression> 
bindSlotByThisScope(UnboundSlot unboundSlot) {
                 if (currentIsInAggregateFunction) {
-                    return bindByAggChild.get().bindSlot(this, unboundSlot);
+                    return bindByAggChild.get().bindSlot(this, unboundSlot, 
false).getBoundSlots();
                 } else {
-                    return 
bindByGroupByThenAggOutputThenAggChild.get().bindSlot(this, unboundSlot);
+                    return bindByGroupByThenAggOutputThenAggChild.get()
+                            .bindSlot(this, unboundSlot, 
false).getBoundSlots();
+                }
+            }
+
+            @Override
+            protected SlotBinding 
bindSlotByRelationQualifierInThisScope(UnboundSlot unboundSlot) {
+                if (currentIsInAggregateFunction) {
+                    return bindByAggChild.get().bindSlot(this, unboundSlot, 
true);
+                } else {
+                    return 
bindByGroupByThenAggOutputThenAggChild.get().bindSlot(this, unboundSlot, true);
                 }
             }
         };
@@ -724,12 +739,14 @@ public class BindExpression implements 
AnalysisRuleFactory {
 
         SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer(
                 having, cascadesContext, defaultScope, false, true,
-                (self, unboundSlot) -> {
-                    List<Expression> slots = self.bindSlotByScope(unboundSlot, 
defaultScope);
-                    if (!slots.isEmpty()) {
+                (self, unboundSlot, bindRelationQualifierOnly) -> {
+                    ExpressionAnalyzer.SlotBinding slots = 
self.bindSlotByScope(
+                            unboundSlot, defaultScope, 
bindRelationQualifierOnly);
+                    if (!slots.getBoundSlots().isEmpty()) {
                         return slots;
                     }
-                    return self.bindSlotByScope(unboundSlot, 
backupScope.get());
+                    return self.bindSlotByScope(unboundSlot, 
backupScope.get(), bindRelationQualifierOnly)
+                            .withQualifierOccupancyFrom(slots);
                 });
         ImmutableSet.Builder<Expression> boundConjuncts = 
ImmutableSet.builder();
         Map<Expression, Expression> bindUniqueIdReplaceMap = 
getBelowAggregateGroupByUniqueFuncReplaceMap(having);
@@ -1358,12 +1375,14 @@ public class BindExpression implements 
AnalysisRuleFactory {
 
         SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer(
                 qualify, cascadesContext, defaultScope.get(), true, true,
-                (self, unboundSlot) -> {
-                List<Expression> slots = self.bindSlotByScope(unboundSlot, 
defaultScope.get());
-                if (!slots.isEmpty()) {
-                    return slots;
-                }
-                return self.bindSlotByScope(unboundSlot, backupScope);
+                (self, unboundSlot, bindRelationQualifierOnly) -> {
+                    ExpressionAnalyzer.SlotBinding slots = 
self.bindSlotByScope(
+                            unboundSlot, defaultScope.get(), 
bindRelationQualifierOnly);
+                    if (!slots.getBoundSlots().isEmpty()) {
+                        return slots;
+                    }
+                    return self.bindSlotByScope(unboundSlot, backupScope, 
bindRelationQualifierOnly)
+                            .withQualifierOccupancyFrom(slots);
                 });
         Map<Expression, Expression> bindUniqueIdReplaceMap = 
getBelowAggregateGroupByUniqueFuncReplaceMap(qualify);
         for (Expression expr : qualify.getConjuncts()) {
@@ -1383,7 +1402,8 @@ public class BindExpression implements 
AnalysisRuleFactory {
         Supplier<CustomSlotBinderAnalyzer> bindByAggChild = 
Suppliers.memoize(() -> {
             Scope aggChildOutputScope
                     = toScope(cascadesContext, 
PlanUtils.fastGetChildrenOutputs(aggregate.children()));
-            return (analyzer, unboundSlot) -> 
analyzer.bindSlotByScope(unboundSlot, aggChildOutputScope);
+            return (analyzer, unboundSlot, bindRelationQualifierOnly) ->
+                    analyzer.bindSlotByScope(unboundSlot, aggChildOutputScope, 
bindRelationQualifierOnly);
         });
         Scope aggOutputScope = toScope(cascadesContext, aggregate.getOutput());
         Supplier<CustomSlotBinderAnalyzer> 
bindByGroupByThenAggOutputThenAggChildOutput = Suppliers.memoize(() -> {
@@ -1396,17 +1416,21 @@ public class BindExpression implements 
AnalysisRuleFactory {
             }
             Scope groupBySlotsScope = toScope(cascadesContext, 
groupBySlots.build());
 
-            return (analyzer, unboundSlot) -> {
-                List<Expression> boundInGroupBy = 
analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope);
-                if (!boundInGroupBy.isEmpty()) {
-                    return ImmutableList.of(boundInGroupBy.get(0));
+            return (analyzer, unboundSlot, bindRelationQualifierOnly) -> {
+                ExpressionAnalyzer.SlotBinding boundInGroupBy = 
analyzer.bindSlotByScope(
+                        unboundSlot, groupBySlotsScope, 
bindRelationQualifierOnly);
+                if (!boundInGroupBy.getBoundSlots().isEmpty()) {
+                    return boundInGroupBy.firstOrEmpty();
                 }
-                List<Expression> boundInAggOutput = 
analyzer.bindSlotByScope(unboundSlot, aggOutputScope);
-                if (!boundInAggOutput.isEmpty()) {
-                    return ImmutableList.of(boundInAggOutput.get(0));
+                ExpressionAnalyzer.SlotBinding boundInAggOutput = 
analyzer.bindSlotByScope(
+                        unboundSlot, aggOutputScope, 
bindRelationQualifierOnly);
+                if (!boundInAggOutput.getBoundSlots().isEmpty()) {
+                    return 
boundInAggOutput.firstOrEmpty().withQualifierOccupancyFrom(boundInGroupBy);
                 }
-                List<? extends Expression> expressions = 
bindByAggChild.get().bindSlot(analyzer, unboundSlot);
-                return expressions.isEmpty() ? expressions : 
ImmutableList.of(expressions.get(0));
+                return bindByAggChild.get().bindSlot(analyzer, unboundSlot, 
bindRelationQualifierOnly)
+                        .firstOrEmpty()
+                        .withQualifierOccupancyFrom(boundInGroupBy)
+                        .withQualifierOccupancyFrom(boundInAggOutput);
             };
         });
 
@@ -1414,7 +1438,13 @@ public class BindExpression implements 
AnalysisRuleFactory {
                 true, true) {
             @Override
             protected List<? extends Expression> 
bindSlotByThisScope(UnboundSlot unboundSlot) {
-                return 
bindByGroupByThenAggOutputThenAggChildOutput.get().bindSlot(this, unboundSlot);
+                return bindByGroupByThenAggOutputThenAggChildOutput.get()
+                        .bindSlot(this, unboundSlot, false).getBoundSlots();
+            }
+
+            @Override
+            protected SlotBinding 
bindSlotByRelationQualifierInThisScope(UnboundSlot unboundSlot) {
+                return 
bindByGroupByThenAggOutputThenAggChildOutput.get().bindSlot(this, unboundSlot, 
true);
             }
         };
 
@@ -1704,27 +1734,30 @@ public class BindExpression implements 
AnalysisRuleFactory {
 
         SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer(
                 agg, cascadesContext, childOutputScope, true, true,
-                (self, unboundSlot) -> {
+                (self, unboundSlot, bindRelationQualifierOnly) -> {
                     // see: https://github.com/apache/doris/pull/15240
                     //
                     // first, try to bind by agg.child.output
-                    List<Expression> slotsInChildren = 
self.bindExactSlotsByThisScope(unboundSlot, childOutputScope);
-                    if (slotsInChildren.size() == 1) {
+                    ExpressionAnalyzer.SlotBinding slotsInChildren = 
self.bindExactSlotsByThisScope(
+                            unboundSlot, childOutputScope, 
bindRelationQualifierOnly);
+                    if (slotsInChildren.getBoundSlots().size() == 1) {
                         // bind succeed
                         return slotsInChildren;
                     }
                     // second, bind failed:
                     // if the slot not found, or more than one candidate slots 
found in agg.child.output,
                     // then try to bind by agg.output
-                    List<Expression> slotsInOutput = 
self.bindExactSlotsByThisScope(unboundSlot, aggOutputScope.get());
-                    if (slotsInOutput.isEmpty()) {
+                    ExpressionAnalyzer.SlotBinding slotsInOutput = 
self.bindExactSlotsByThisScope(
+                            unboundSlot, aggOutputScope.get(), 
bindRelationQualifierOnly);
+                    if (slotsInOutput.getBoundSlots().isEmpty()) {
                         // if slotsInChildren.size() > 1 && 
slotsInOutput.isEmpty(),
                         // we return slotsInChildren to throw an ambiguous 
slots exception
-                        return slotsInChildren;
+                        return 
slotsInChildren.withQualifierOccupancyFrom(slotsInOutput);
                     }
 
-                    Builder<Expression> useOutputExpr = 
ImmutableList.builderWithExpectedSize(slotsInOutput.size());
-                    for (Expression slotInOutput : slotsInOutput) {
+                    Builder<Expression> useOutputExpr = 
ImmutableList.builderWithExpectedSize(
+                            slotsInOutput.getBoundSlots().size());
+                    for (Expression slotInOutput : 
slotsInOutput.getBoundSlots()) {
                         // mappingSlot is provided by aggOutputScope
                         // and no non-MappingSlot slot exist in the Scope, so 
we
                         // can direct cast it safely
@@ -1741,7 +1774,9 @@ public class BindExpression implements 
AnalysisRuleFactory {
                         // we should rewrite to: select k + 1 as k1 from tbl 
group by k + 1
                         useOutputExpr.add(mappingSlot.getMappingExpression());
                     }
-                    return useOutputExpr.build();
+                    return new 
ExpressionAnalyzer.SlotBinding(useOutputExpr.build(), false)
+                            .withQualifierOccupancyFrom(slotsInChildren)
+                            .withQualifierOccupancyFrom(slotsInOutput);
                 });
 
         ImmutableList.Builder<Expression> boundGroupByBuilder = 
ImmutableList.builderWithExpectedSize(groupBy.size());
@@ -1816,17 +1851,20 @@ public class BindExpression implements 
AnalysisRuleFactory {
                 () -> toScope(cascadesContext, 
PlanUtils.fastGetChildrenOutputs(finalInput.children())));
         SimpleExprAnalyzer bindInInputScopeThenInputChildScope = 
buildCustomSlotBinderAnalyzer(
                 sort, cascadesContext, inputScope, true, false,
-                (self, unboundSlot) -> {
+                (self, unboundSlot, bindRelationQualifierOnly) -> {
                     // first, try to bind slot in Scope(input.output)
-                    List<Expression> slotsInInput = 
self.bindExactSlotsByThisScope(unboundSlot, inputScope);
-                    if (!slotsInInput.isEmpty()) {
+                    ExpressionAnalyzer.SlotBinding slotsInInput = 
self.bindExactSlotsByThisScope(
+                            unboundSlot, inputScope, 
bindRelationQualifierOnly);
+                    if (!slotsInInput.getBoundSlots().isEmpty()) {
                         // bind succeed
-                        return ImmutableList.of(slotsInInput.get(0));
+                        return slotsInInput.firstOrEmpty();
                     }
                     // second, bind failed:
                     // if the slot not found, or more than one candidate slots 
found in input.output,
                     // then try to bind by input.children.output
-                    return self.bindExactSlotsByThisScope(unboundSlot, 
inputChildrenScope.get());
+                    return self.bindExactSlotsByThisScope(
+                            unboundSlot, inputChildrenScope.get(), 
bindRelationQualifierOnly)
+                            .withQualifierOccupancyFrom(slotsInInput);
                 });
 
         SimpleExprAnalyzer bindInInputChildScope = 
getAnalyzerForOrderByAggFunc(finalInput, cascadesContext, sort,
@@ -1951,7 +1989,12 @@ public class BindExpression implements 
AnalysisRuleFactory {
                 enableExactMatch, bindSlotInOuterScope) {
             @Override
             protected List<? extends Expression> 
bindSlotByThisScope(UnboundSlot unboundSlot) {
-                return customSlotBinder.bindSlot(this, unboundSlot);
+                return customSlotBinder.bindSlot(this, unboundSlot, 
false).getBoundSlots();
+            }
+
+            @Override
+            protected SlotBinding 
bindSlotByRelationQualifierInThisScope(UnboundSlot unboundSlot) {
+                return customSlotBinder.bindSlot(this, unboundSlot, true);
             }
         };
         return expr -> expressionAnalyzer.analyze(expr, rewriteContext);
@@ -1979,7 +2022,8 @@ public class BindExpression implements 
AnalysisRuleFactory {
     }
 
     private interface CustomSlotBinderAnalyzer {
-        List<? extends Expression> bindSlot(ExpressionAnalyzer analyzer, 
UnboundSlot unboundSlot);
+        ExpressionAnalyzer.SlotBinding bindSlot(
+                ExpressionAnalyzer analyzer, UnboundSlot unboundSlot, boolean 
bindRelationQualifierOnly);
     }
 
     public String toSqlWithBackquote(List<Slot> slots) {
@@ -2019,15 +2063,19 @@ public class BindExpression implements 
AnalysisRuleFactory {
         Scope outputWithoutAggFunc = toScope(cascadesContext, 
outputSlots.build());
         SimpleExprAnalyzer bindInInputChildScope = 
buildCustomSlotBinderAnalyzer(
                 sort, cascadesContext, inputScope, true, false,
-                (analyzer, unboundSlot) -> {
+                (analyzer, unboundSlot, bindRelationQualifierOnly) -> {
                     if (finalInput instanceof LogicalAggregate) {
-                        List<Expression> boundInOutputWithoutAggFunc = 
analyzer.bindSlotByScope(unboundSlot,
-                                outputWithoutAggFunc);
-                        if (!boundInOutputWithoutAggFunc.isEmpty()) {
-                            return 
ImmutableList.of(boundInOutputWithoutAggFunc.get(0));
+                        ExpressionAnalyzer.SlotBinding 
boundInOutputWithoutAggFunc = analyzer.bindSlotByScope(
+                                unboundSlot, outputWithoutAggFunc, 
bindRelationQualifierOnly);
+                        if 
(!boundInOutputWithoutAggFunc.getBoundSlots().isEmpty()) {
+                            return boundInOutputWithoutAggFunc.firstOrEmpty();
                         }
+                        return analyzer.bindExactSlotsByThisScope(
+                                unboundSlot, inputChildrenScope.get(), 
bindRelationQualifierOnly)
+                                
.withQualifierOccupancyFrom(boundInOutputWithoutAggFunc);
                     }
-                    return analyzer.bindExactSlotsByThisScope(unboundSlot, 
inputChildrenScope.get());
+                    return analyzer.bindExactSlotsByThisScope(
+                            unboundSlot, inputChildrenScope.get(), 
bindRelationQualifierOnly);
                 });
         return bindInInputChildScope;
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
index bf7e872de20..569dd874a44 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
@@ -124,6 +124,8 @@ import org.apache.commons.lang3.StringUtils;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
@@ -312,14 +314,36 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
     @Override
     public Expression visitUnboundSlot(UnboundSlot unboundSlot, 
ExpressionRewriteContext context) {
         Optional<Scope> outerScope = getScope().getOuterScope();
-        Optional<List<? extends Expression>> boundedOpt = 
Optional.of(bindSlotByThisScope(unboundSlot));
-        boolean foundInThisScope = !boundedOpt.get().isEmpty();
+        List<? extends Expression> bounded = ImmutableList.of();
+        boolean foundInThisScope = false;
+        boolean canBindOuterScope = bindSlotInOuterScope && 
outerScope.isPresent();
+        boolean relationQualifierOccupied = false;
+
+        // A multipart name can be either a relation-qualified column (t.col) 
or a nested field
+        // reference (col.field). In a correlated subquery, try the 
relation-qualified interpretation
+        // in both visible scopes first, so an inner column named "t" does not 
hide an outer alias "t".
+        if (canBindOuterScope && shouldPrioritizeRelationQualifier()
+                && unboundSlot.getNameParts().size() > 1) {
+            SlotBinding localRelationBinding = 
bindSlotByRelationQualifierInThisScope(unboundSlot);
+            bounded = localRelationBinding.getBoundSlots();
+            foundInThisScope = !bounded.isEmpty();
+            if (!foundInThisScope) {
+                relationQualifierOccupied = 
localRelationBinding.isRelationQualifierOccupied();
+            }
+            if (!foundInThisScope && !relationQualifierOccupied) {
+                bounded = bindSlotsByRelationQualifier(unboundSlot, 
outerScope.get());
+            }
+        }
+
+        if (bounded.isEmpty()) {
+            bounded = bindSlotByThisScope(unboundSlot);
+            foundInThisScope = !bounded.isEmpty();
+        }
         // Currently only looking for symbols on the previous level.
-        if (bindSlotInOuterScope && !foundInThisScope && 
outerScope.isPresent()) {
-            boundedOpt = Optional.of(bindSlotByScope(unboundSlot, 
outerScope.get()));
+        if (canBindOuterScope && bounded.isEmpty() && 
!relationQualifierOccupied) {
+            bounded = bindSlotByScope(unboundSlot, outerScope.get());
         }
         // it is heavy to deduplicate slots in scope. So we deduplicates 
bounded here
-        List<? extends Expression> bounded = boundedOpt.get();
         if (bounded.size() > 1) {
             bounded = bounded.stream().distinct().collect(Collectors.toList());
         }
@@ -333,14 +357,15 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
                 return unboundSlot;
             case 1:
                 Expression firstBound = bounded.get(0);
-                if (!foundInThisScope && firstBound instanceof Slot
-                        && 
!outerScope.get().getCorrelatedSlots().contains(firstBound)) {
+                Set<Slot> inputSlots = firstBound.getInputSlots();
+                if (!foundInThisScope
+                        && 
!outerScope.get().getCorrelatedSlots().containsAll(inputSlots)) {
                     if (currentPlan instanceof LogicalJoin) {
                         throw new AnalysisException(
                                 "Unsupported correlated subquery with 
correlated slot in join conjuncts "
                                         + currentPlan);
                     }
-                    outerScope.get().getCorrelatedSlots().add((Slot) 
firstBound);
+                    outerScope.get().getCorrelatedSlots().addAll(inputSlots);
                 }
                 if (firstBound.getDataType() instanceof NestedColumnPrunable
                         || firstBound.getDataType().isVariantType()) {
@@ -456,6 +481,11 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
         ExpressionAnalyzer lambdaAnalyzer = new 
ExpressionAnalyzer(currentPlan, new Scope(Optional.of(getScope()),
                 boundedSlots), context == null ? null : 
context.cascadesContext,
                 true, true) {
+            @Override
+            protected boolean shouldPrioritizeRelationQualifier() {
+                return false;
+            }
+
             @Override
             protected void couldNotFoundColumn(UnboundSlot unboundSlot, String 
tableName) {
                 throw new AnalysisException("Unknown lambda slot '"
@@ -466,6 +496,11 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
         return lambdaAnalyzer.analyze(lambdaFunction, context);
     }
 
+    /** Whether relation-qualified columns should be resolved across scopes 
before nested fields. */
+    protected boolean shouldPrioritizeRelationQualifier() {
+        return true;
+    }
+
     UnboundFunction preProcessUnboundFunction(UnboundFunction unboundFunction, 
ExpressionRewriteContext context) {
         // NOTICE: some trick code here. because for time arithmetic functions,
         //  the first argument of them is TimeUnit, but is cannot distinguish 
with UnboundSlot in parser.
@@ -1165,17 +1200,27 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
         return bindSlotByScope(unboundSlot, getScope());
     }
 
+    protected SlotBinding bindSlotByRelationQualifierInThisScope(UnboundSlot 
unboundSlot) {
+        return bindSlotByRelationQualifier(unboundSlot, getScope());
+    }
+
     protected List<Expression> bindExactSlotsByThisScope(UnboundSlot 
unboundSlot, Scope scope) {
-        List<Expression> candidates = bindSlotByScope(unboundSlot, scope);
+        return bindExactSlotsByThisScope(unboundSlot, scope, 
false).getBoundSlots();
+    }
+
+    protected SlotBinding bindExactSlotsByThisScope(
+            UnboundSlot unboundSlot, Scope scope, boolean 
bindRelationQualifierOnly) {
+        SlotBinding binding = bindSlotByScope(unboundSlot, scope, 
bindRelationQualifierOnly);
+        List<Expression> candidates = binding.getBoundSlots();
         if (candidates.size() == 1) {
-            return candidates;
+            return binding;
         }
         List<Expression> extractSlots = Utils.filterImmutableList(candidates, 
bound ->
                 bound instanceof Slot && unboundSlot.getNameParts().size() == 
((Slot) bound).getQualifier().size() + 1
         );
         // we should return origin candidates slots if extract slots is empty,
         // and then throw an ambiguous exception
-        return !extractSlots.isEmpty() ? extractSlots : candidates;
+        return binding.withBoundSlots(!extractSlots.isEmpty() ? extractSlots : 
candidates);
     }
 
     private List<Slot> addSqlIndexInfo(List<Slot> slots, 
Optional<Pair<Integer, Integer>> indexInSql) {
@@ -1214,12 +1259,128 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
         }
     }
 
+    protected SlotBinding bindSlotByScope(
+            UnboundSlot unboundSlot, Scope scope, boolean 
bindRelationQualifierOnly) {
+        return bindRelationQualifierOnly
+                ? bindSlotByRelationQualifier(unboundSlot, scope)
+                : new SlotBinding(bindSlotByScope(unboundSlot, scope), false);
+    }
+
+    /** Bind a multipart slot as a relation-qualified column, without treating 
its first part as a column. */
+    protected SlotBinding bindSlotByRelationQualifier(UnboundSlot unboundSlot, 
Scope scope) {
+        List<? extends Expression> bounded = 
bindSlotsByRelationQualifier(unboundSlot, scope);
+        return bounded.isEmpty()
+                ? new SlotBinding(bounded,
+                        () -> 
containsRelationQualifier(unboundSlot.getNameParts(), scope))
+                : new SlotBinding(bounded, false);
+    }
+
+    private List<? extends Expression> 
bindSlotsByRelationQualifier(UnboundSlot unboundSlot, Scope scope) {
+        List<String> nameParts = unboundSlot.getNameParts();
+        Optional<Pair<Integer, Integer>> idxInSql = 
unboundSlot.getIndexInSqlString();
+        List<? extends Expression> bounded;
+        switch (nameParts.size()) {
+            case 1:
+                bounded = ImmutableList.of();
+                break;
+            case 2:
+                bounded = bindExpressionByTableColumn(
+                        unboundSlot, nameParts, idxInSql, scope, false);
+                break;
+            case 3:
+                bounded = bindExpressionByDbTableColumn(
+                        unboundSlot, nameParts, idxInSql, scope, false);
+                break;
+            default:
+                bounded = bindExpressionByCatalogDbTableColumn(
+                        unboundSlot, nameParts, idxInSql, scope, false);
+                break;
+        }
+        return bounded;
+    }
+
+    private boolean containsRelationQualifier(List<String> nameParts, Scope 
scope) {
+        int lastRelationNameIndex = Math.min(2, nameParts.size() - 2);
+        for (int relationNameIndex = 0; relationNameIndex <= 
lastRelationNameIndex; relationNameIndex++) {
+            for (List<String> qualifier
+                    : 
scope.findRelationQualifiersIgnoreCase(nameParts.get(relationNameIndex))) {
+                String catalogName = extractCatalogName(qualifier);
+                int lowerCaseTableNames = 
resolveLowerCaseTableNames(catalogName);
+                int lowerCaseDatabaseNames = 
resolveLowerCaseDatabaseNames(catalogName);
+                if (nameParts.size() >= 4 && qualifier.size() >= 3
+                        && qualifier.get(qualifier.size() - 
3).equalsIgnoreCase(nameParts.get(0))
+                        && 
compareDbNameIgnoreClusterName(qualifier.get(qualifier.size() - 2),
+                                nameParts.get(1), lowerCaseDatabaseNames)
+                        && sameTableName(qualifier.get(qualifier.size() - 1),
+                                nameParts.get(2), lowerCaseTableNames)) {
+                    return true;
+                }
+                if (nameParts.size() >= 3 && qualifier.size() >= 2
+                        && 
compareDbNameIgnoreClusterName(qualifier.get(qualifier.size() - 2),
+                                nameParts.get(0), lowerCaseDatabaseNames)
+                        && sameTableName(qualifier.get(qualifier.size() - 1),
+                                nameParts.get(1), lowerCaseTableNames)) {
+                    return true;
+                }
+                if (sameTableName(qualifier.get(qualifier.size() - 1),
+                        nameParts.get(0), lowerCaseTableNames)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /** Relation-qualified binding candidates and whether that qualifier 
exists in the searched scope. */
+    protected static class SlotBinding {
+        private final List<Expression> boundSlots;
+        private final Supplier<Boolean> relationQualifierOccupied;
+
+        protected SlotBinding(List<? extends Expression> boundSlots, boolean 
relationQualifierOccupied) {
+            this(boundSlots, () -> relationQualifierOccupied);
+        }
+
+        private SlotBinding(List<? extends Expression> boundSlots, 
Supplier<Boolean> relationQualifierOccupied) {
+            this.boundSlots = ImmutableList.copyOf(boundSlots);
+            this.relationQualifierOccupied = relationQualifierOccupied;
+        }
+
+        protected List<Expression> getBoundSlots() {
+            return boundSlots;
+        }
+
+        protected boolean isRelationQualifierOccupied() {
+            return relationQualifierOccupied.get();
+        }
+
+        protected SlotBinding firstOrEmpty() {
+            return boundSlots.isEmpty()
+                    ? this
+                    : new SlotBinding(ImmutableList.of(boundSlots.get(0)), 
relationQualifierOccupied);
+        }
+
+        private SlotBinding withBoundSlots(List<? extends Expression> 
boundSlots) {
+            return new SlotBinding(boundSlots, relationQualifierOccupied);
+        }
+
+        protected SlotBinding withQualifierOccupancyFrom(SlotBinding other) {
+            return new SlotBinding(boundSlots,
+                    () -> relationQualifierOccupied.get() || 
other.relationQualifierOccupied.get());
+        }
+    }
+
     private List<? extends Expression> bindExpressionByCatalogDbTableColumn(
             UnboundSlot unboundSlot, List<String> nameParts, 
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+        return bindExpressionByCatalogDbTableColumn(unboundSlot, nameParts, 
idxInSql, scope, true);
+    }
+
+    private List<? extends Expression> bindExpressionByCatalogDbTableColumn(
+            UnboundSlot unboundSlot, List<String> nameParts, 
Optional<Pair<Integer, Integer>> idxInSql,
+            Scope scope, boolean fallbackToColumn) {
         List<Slot> slots = bindSingleSlotByCatalog(
                         nameParts.get(0), nameParts.get(1), nameParts.get(2), 
nameParts.get(3), scope);
         if (slots.isEmpty()) {
-            return bindExpressionByDbTableColumn(unboundSlot, nameParts, 
idxInSql, scope);
+            return bindExpressionByDbTableColumn(unboundSlot, nameParts, 
idxInSql, scope, fallbackToColumn);
         } else if (slots.size() > 1) {
             return addSqlIndexInfo(slots, idxInSql);
         }
@@ -1238,9 +1399,15 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
 
     private List<? extends Expression> bindExpressionByDbTableColumn(
             UnboundSlot unboundSlot, List<String> nameParts, 
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+        return bindExpressionByDbTableColumn(unboundSlot, nameParts, idxInSql, 
scope, true);
+    }
+
+    private List<? extends Expression> bindExpressionByDbTableColumn(
+            UnboundSlot unboundSlot, List<String> nameParts, 
Optional<Pair<Integer, Integer>> idxInSql,
+            Scope scope, boolean fallbackToColumn) {
         List<Slot> slots = bindSingleSlotByDb(nameParts.get(0), 
nameParts.get(1), nameParts.get(2), scope);
         if (slots.isEmpty()) {
-            return bindExpressionByTableColumn(unboundSlot, nameParts, 
idxInSql, scope);
+            return bindExpressionByTableColumn(unboundSlot, nameParts, 
idxInSql, scope, fallbackToColumn);
         } else if (slots.size() > 1) {
             return addSqlIndexInfo(slots, idxInSql);
         }
@@ -1259,9 +1426,17 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
 
     private List<? extends Expression> bindExpressionByTableColumn(
             UnboundSlot unboundSlot, List<String> nameParts, 
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+        return bindExpressionByTableColumn(unboundSlot, nameParts, idxInSql, 
scope, true);
+    }
+
+    private List<? extends Expression> bindExpressionByTableColumn(
+            UnboundSlot unboundSlot, List<String> nameParts, 
Optional<Pair<Integer, Integer>> idxInSql,
+            Scope scope, boolean fallbackToColumn) {
         List<Slot> slots = bindSingleSlotByTable(nameParts.get(0), 
nameParts.get(1), scope);
         if (slots.isEmpty()) {
-            return bindExpressionByColumn(unboundSlot, nameParts, idxInSql, 
scope);
+            return fallbackToColumn
+                    ? bindExpressionByColumn(unboundSlot, nameParts, idxInSql, 
scope)
+                    : ImmutableList.of();
         } else if (slots.size() > 1) {
             return addSqlIndexInfo(slots, idxInSql);
         }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/analyzer/ScopeTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/analyzer/ScopeTest.java
new file mode 100644
index 00000000000..859874bdbe8
--- /dev/null
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/analyzer/ScopeTest.java
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.analyzer;
+
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.types.IntegerType;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+class ScopeTest {
+
+    @Test
+    void testFindRelationQualifiersIgnoreCase() {
+        List<String> qualifier1 = ImmutableList.of("internal", "db", "t1");
+        List<String> qualifier2 = ImmutableList.of("internal", "db", "t2");
+        Scope scope = new Scope(ImmutableList.of(
+                new SlotReference(new ExprId(1), "c1", IntegerType.INSTANCE, 
true, qualifier1),
+                new SlotReference(new ExprId(2), "c2", IntegerType.INSTANCE, 
true, qualifier1),
+                new SlotReference(new ExprId(3), "c1", IntegerType.INSTANCE, 
true, qualifier2),
+                new SlotReference(new ExprId(4), "unqualified", 
IntegerType.INSTANCE, true, ImmutableList.of())));
+
+        Assertions.assertEquals(ImmutableList.of(qualifier1),
+                
ImmutableList.copyOf(scope.findRelationQualifiersIgnoreCase("T1")));
+        Assertions.assertEquals(ImmutableList.of(qualifier2),
+                
ImmutableList.copyOf(scope.findRelationQualifiersIgnoreCase("t2")));
+        
Assertions.assertTrue(scope.findRelationQualifiersIgnoreCase("unqualified").isEmpty());
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
index 4f15f00ab56..c09ad82fb65 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
@@ -51,9 +51,60 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
+import java.util.Optional;
+import java.util.Set;
 
 public class ExpressionAnalyzerTest {
 
+    @Test
+    void testSkipQualifierOccupancyForLocalBindingHit() {
+        SlotReference localSlot = new SlotReference(
+                new ExprId(1), "c", BigIntType.INSTANCE, true, 
ImmutableList.of("t"));
+        Scope outerScope = new Scope(ImmutableList.of());
+        Scope localScope = new Scope(Optional.of(outerScope), 
ImmutableList.of(localSlot)) {
+            @Override
+            public Set<List<String>> findRelationQualifiersIgnoreCase(String 
relationName) {
+                throw new AssertionError("Qualifier occupancy should not be 
evaluated for a binding hit");
+            }
+        };
+        ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, localScope, 
null, true, true);
+
+        Assertions.assertEquals(localSlot, analyzer.analyze(new 
UnboundSlot("t", "c")));
+    }
+
+    @Test
+    void testOuterRelationProbeDoesNotEvaluateQualifierOccupancy() {
+        SlotReference outerSlot = new SlotReference(
+                new ExprId(1), "c", BigIntType.INSTANCE, true, 
ImmutableList.of("t"));
+        Scope outerScope = new Scope(ImmutableList.of(outerSlot)) {
+            @Override
+            public Set<List<String>> findRelationQualifiersIgnoreCase(String 
relationName) {
+                throw new AssertionError("Outer relation probe should only 
bind slots");
+            }
+        };
+        Scope localScope = new Scope(Optional.of(outerScope), 
ImmutableList.of());
+        ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, localScope, 
null, true, true);
+
+        Assertions.assertEquals(outerSlot, analyzer.analyze(new 
UnboundSlot("t", "c")));
+        Assertions.assertEquals(ImmutableList.of(outerSlot), 
ImmutableList.copyOf(outerScope.getCorrelatedSlots()));
+    }
+
+    @Test
+    void testKeepQualifierOccupancyLazyInExactBinding() {
+        Scope scope = new Scope(ImmutableList.of()) {
+            @Override
+            public Set<List<String>> findRelationQualifiersIgnoreCase(String 
relationName) {
+                throw new AssertionError("Exact binding should preserve lazy 
qualifier occupancy");
+            }
+        };
+        ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, scope, 
null, true, true);
+
+        ExpressionAnalyzer.SlotBinding binding = Assertions.assertDoesNotThrow(
+                () -> analyzer.bindExactSlotsByThisScope(new UnboundSlot("t", 
"c"), scope, true));
+        Assertions.assertTrue(binding.getBoundSlots().isEmpty());
+        Assertions.assertThrows(AssertionError.class, 
binding::isRelationQualifierOccupied);
+    }
+
     @Test
     void testPreProcessUnboundFunctionForThreeArgsDataTimeFunction() {
         ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, new 
Scope(ImmutableList.of()),
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
index 4731a4c988b..ba932cb5b4c 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
@@ -17,18 +17,30 @@
 
 package org.apache.doris.nereids.rules.analysis;
 
+import org.apache.doris.catalog.ArrayType;
 import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.StructField;
+import org.apache.doris.catalog.StructType;
+import org.apache.doris.catalog.Type;
 import org.apache.doris.catalog.VariantType;
 import org.apache.doris.common.FeConstants;
 import 
org.apache.doris.datasource.test.TestExternalCatalog.TestCatalogProvider;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import 
org.apache.doris.nereids.trees.expressions.ArrayItemReference.ArrayItemSlot;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ElementAt;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Lambda;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalApply;
 import org.apache.doris.nereids.util.PlanChecker;
 import org.apache.doris.utframe.TestWithFeService;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -39,6 +51,23 @@ public class TestDereference extends TestWithFeService {
                     "t", ImmutableList.of(
                             new Column("id", PrimitiveType.INT),
                             new Column("t", new VariantType())
+                    ),
+                    "outer_table", ImmutableList.of(
+                            new Column("id", PrimitiveType.INT),
+                            new Column("value", PrimitiveType.INT),
+                            new Column("@event_name", PrimitiveType.VARCHAR),
+                            new Column("payload", new StructType(new 
StructField("k", Type.INT))),
+                            new Column("items", new ArrayType(
+                                    new StructType(new StructField("value", 
Type.INT))))
+                    ),
+                    "inner_table", ImmutableList.of(
+                            new Column("id", PrimitiveType.INT),
+                            new Column("t1", PrimitiveType.INT),
+                            new Column("t", new StructType(new 
StructField("value", Type.INT)))
+                    ),
+                    "inner_variant_table", ImmutableList.of(
+                            new Column("id", PrimitiveType.INT),
+                            new Column("outer_alias", new VariantType())
                     )
             )
     );
@@ -70,6 +99,112 @@ public class TestDereference extends TestWithFeService {
         testBind("select t.t.t.t.t.t from t");
     }
 
+    @Test
+    public void testCorrelatedSubqueryPrefersOuterTableAlias() {
+        testBind("select t1.`@event_name` from outer_table t1 where exists ("
+                + "select 1 from inner_table inner_alias where 
t1.`@event_name` = 'click')");
+    }
+
+    @Test
+    public void testOuterTableAliasTakesPriorityOverInnerVariantColumn() {
+        Plan plan = PlanChecker.from(connectContext)
+                .analyze("select outer_alias.id from outer_table outer_alias 
where exists ("
+                        + "select 1 from inner_variant_table inner_alias where 
outer_alias.value = 1)")
+                .getPlan();
+
+        LogicalApply<?, ?> apply = getOnlyApply(plan);
+        Assertions.assertEquals(1, apply.getCorrelationSlot().size());
+        Assertions.assertEquals("value", 
apply.getCorrelationSlot().get(0).getName());
+        List<String> qualifier = 
apply.getCorrelationSlot().get(0).getQualifier();
+        Assertions.assertEquals("outer_alias", qualifier.get(qualifier.size() 
- 1));
+    }
+
+    @Test
+    public void testInnerAliasShadowsOuterAliasInFilter() {
+        Plan plan = PlanChecker.from(connectContext)
+                .analyze("select t.id from outer_table t where exists ("
+                        + "select 1 from inner_table t where t.id = 1)")
+                .getPlan();
+
+        
Assertions.assertTrue(getOnlyApply(plan).getCorrelationSlot().isEmpty());
+    }
+
+    @Test
+    public void testInnerAliasKeepsNestedFieldFallback() {
+        Plan plan = PlanChecker.from(connectContext)
+                .analyze("select t.id from outer_table t where exists ("
+                        + "select 1 from inner_table t where t.value = 1)")
+                .getPlan();
+
+        
Assertions.assertTrue(getOnlyApply(plan).getCorrelationSlot().isEmpty());
+    }
+
+    @Test
+    public void testInnerAliasKeepsScalarFieldError() {
+        AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                () -> PlanChecker.from(connectContext)
+                        .analyze("select t1.id from outer_table t1 where 
exists ("
+                                + "select 1 from inner_table t1 where 
t1.`@event_name` = 'click')"));
+        Assertions.assertTrue(exception.getMessage().contains("No such field 
'@event_name' in 't1'"));
+    }
+
+    @Test
+    public void testLambdaArgumentTakesPriorityOverOuterTableAlias() {
+        Plan plan = PlanChecker.from(connectContext)
+                .analyze("select array_map(x -> x.value, x.items) from 
outer_table x")
+                .getPlan();
+
+        List<Lambda> lambdas = new ArrayList<>();
+        for (Plan node : plan.<Plan>collectToList(ignored -> true)) {
+            node.getExpressions().forEach(expression ->
+                    
lambdas.addAll(expression.collectToList(Lambda.class::isInstance)));
+        }
+        Assertions.assertEquals(1, lambdas.size());
+        
Assertions.assertTrue(lambdas.get(0).getLambdaFunction().containsType(ElementAt.class));
+        
Assertions.assertTrue(lambdas.get(0).getLambdaFunction().anyMatch(ArrayItemSlot.class::isInstance));
+    }
+
+    @Test
+    public void testOuterNestedFieldRegistersCorrelationSlot() {
+        Plan plan = PlanChecker.from(connectContext)
+                .analyze("select outer_alias.id from outer_table outer_alias 
where exists ("
+                        + "select 1 from inner_variant_table inner_alias where 
outer_alias.payload.k = 1)")
+                .getPlan();
+
+        LogicalApply<?, ?> apply = getOnlyApply(plan);
+        Assertions.assertEquals(1, apply.getCorrelationSlot().size());
+        Assertions.assertEquals("payload", 
apply.getCorrelationSlot().get(0).getName());
+        List<String> qualifier = 
apply.getCorrelationSlot().get(0).getQualifier();
+        Assertions.assertEquals("outer_alias", qualifier.get(qualifier.size() 
- 1));
+    }
+
+    @Test
+    public void testInnerHavingAliasShadowsOuterAlias() {
+        Plan plan = PlanChecker.from(connectContext)
+                .analyze("select t.id from outer_table t where exists ("
+                        + "select 1 from inner_table t having max(t.id) > 0)")
+                .getPlan();
+
+        
Assertions.assertTrue(getOnlyApply(plan).getCorrelationSlot().isEmpty());
+    }
+
+    @Test
+    public void testInnerQualifyAliasShadowsOuterAlias() {
+        Plan plan = PlanChecker.from(connectContext)
+                .analyze("select t.id from outer_table t where exists ("
+                        + "select 1 from inner_table t group by t.id "
+                        + "qualify row_number() over (order by id) = t.id)")
+                .getPlan();
+
+        
Assertions.assertTrue(getOnlyApply(plan).getCorrelationSlot().isEmpty());
+    }
+
+    private LogicalApply<?, ?> getOnlyApply(Plan plan) {
+        List<LogicalApply<?, ?>> applies = 
plan.collectToList(LogicalApply.class::isInstance);
+        Assertions.assertEquals(1, applies.size());
+        return applies.get(0);
+    }
+
     private void testBind(String sql) {
         PlanChecker.from(connectContext)
                 .analyze(sql)
diff --git a/regression-test/data/query_p0/test_dereference.out 
b/regression-test/data/query_p0/test_dereference.out
new file mode 100644
index 00000000000..0bdaece517c
--- /dev/null
+++ b/regression-test/data/query_p0/test_dereference.out
@@ -0,0 +1,40 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !correlated_scalar_alias --
+2      kept
+
+-- !correlated_complex_alias --
+2      20
+
+-- !correlated_db_table_qualifier --
+2
+
+-- !correlated_catalog_db_table_qualifier --
+1
+
+-- !lambda_alias --
+1      [1, 2]
+2      [3]
+
+-- !nested_correlation --
+1
+
+-- !having_inner_alias --
+1
+2
+
+-- !qualify_inner_alias --
+1
+2
+
+-- !filter_inner_alias --
+1
+2
+
+-- !filter_inner_nested_field --
+1
+2
+
+-- !group_by_inner_nested_field --
+1
+2
+
diff --git a/regression-test/suites/query_p0/test_dereference.groovy 
b/regression-test/suites/query_p0/test_dereference.groovy
index 30c123e3c37..d6c855ac217 100644
--- a/regression-test/suites/query_p0/test_dereference.groovy
+++ b/regression-test/suites/query_p0/test_dereference.groovy
@@ -66,4 +66,170 @@ suite("test_dereference") {
         sql "select s.a from test_dereference2"
         exception "No such struct field 'a' in 's'"
     }
-}
\ No newline at end of file
+
+    multi_sql """
+        drop table if exists test_correlated_dereference_outer;
+        drop table if exists test_correlated_dereference_inner_scalar;
+        drop table if exists test_correlated_dereference_inner_struct;
+        create table test_correlated_dereference_outer(
+          id int,
+          value int,
+          `@event_name` varchar(32),
+          payload struct<k:int>,
+          items array<struct<value:int>>
+        )
+        distributed by hash(id)
+        properties('replication_num'='1');
+
+        create table test_correlated_dereference_inner_scalar(
+          id int,
+          t1 int,
+          t struct<value:int>,
+          `${context.dbName}` 
struct<test_correlated_dereference_outer:struct<value:int>>,
+          internal 
struct<`${context.dbName}`:struct<test_correlated_dereference_outer:struct<value:int>>>
+        )
+        distributed by hash(id)
+        properties('replication_num'='1');
+
+        create table test_correlated_dereference_inner_struct(
+          id int,
+          outer_alias struct<value:int>
+        )
+        distributed by hash(id)
+        properties('replication_num'='1');
+
+        insert into test_correlated_dereference_outer values
+            (1, 10, 'blocked', struct(1), array(struct(1), struct(2))),
+            (2, 20, 'kept', struct(2), array(struct(3)));
+        insert into test_correlated_dereference_inner_scalar values
+            (1, 0, struct(1), struct(struct(0)), struct(struct(struct(0)))),
+            (1, 0, struct(2), struct(struct(0)), struct(struct(struct(0))));
+        insert into test_correlated_dereference_inner_struct values (1, 
struct(10));
+        """
+
+    order_qt_correlated_scalar_alias """
+            select t1.id, t1.`@event_name`
+            from test_correlated_dereference_outer t1
+            where not exists (
+                select 1 from test_correlated_dereference_inner_scalar 
inner_alias
+                where t1.`@event_name` = 'blocked'
+            )
+            order by t1.id
+            """
+
+    order_qt_correlated_complex_alias """
+            select outer_alias.id, outer_alias.value
+            from test_correlated_dereference_outer outer_alias
+            where not exists (
+                select 1 from test_correlated_dereference_inner_struct 
inner_alias
+                where outer_alias.value = 10
+            )
+            order by outer_alias.id
+            """
+
+    order_qt_correlated_db_table_qualifier """
+            select test_correlated_dereference_outer.id
+            from test_correlated_dereference_outer
+            where not exists (
+                select 1 from test_correlated_dereference_inner_scalar 
inner_alias
+                where 
`${context.dbName}`.`test_correlated_dereference_outer`.value = 10
+            )
+            order by test_correlated_dereference_outer.id
+            """
+
+    order_qt_correlated_catalog_db_table_qualifier """
+            select test_correlated_dereference_outer.id
+            from test_correlated_dereference_outer
+            where not exists (
+                select 1 from test_correlated_dereference_inner_scalar 
inner_alias
+                where 
internal.`${context.dbName}`.`test_correlated_dereference_outer`.value = 20
+            )
+            order by test_correlated_dereference_outer.id
+            """
+
+    order_qt_lambda_alias """
+            select x.id, array_map(x -> x.value, x.items)
+            from test_correlated_dereference_outer x
+            order by x.id
+            """
+
+    order_qt_nested_correlation """
+            select outer_alias.id
+            from test_correlated_dereference_outer outer_alias
+            where exists (
+                select 1 from test_correlated_dereference_inner_struct 
inner_alias
+                where outer_alias.payload.k = 1
+            )
+            order by outer_alias.id
+            """
+
+    order_qt_having_inner_alias """
+            select t.id
+            from test_correlated_dereference_outer t
+            where exists (
+                select 1
+                from test_correlated_dereference_inner_scalar t
+                having max(t.id) < 2
+            )
+            order by t.id
+            """
+
+    order_qt_qualify_inner_alias """
+            select t.id
+            from test_correlated_dereference_outer t
+            where exists (
+                select 1
+                from test_correlated_dereference_inner_scalar t
+                group by t.id
+                qualify row_number() over (order by id) = t.id
+            )
+            order by t.id
+            """
+
+    order_qt_filter_inner_alias """
+            select t.id
+            from test_correlated_dereference_outer t
+            where exists (
+                select 1
+                from test_correlated_dereference_inner_scalar t
+                where t.id = 1
+            )
+            order by t.id
+            """
+
+    order_qt_filter_inner_nested_field """
+            select t.id
+            from test_correlated_dereference_outer t
+            where exists (
+                select 1
+                from test_correlated_dereference_inner_scalar t
+                where t.value = 1
+            )
+            order by t.id
+            """
+
+    order_qt_group_by_inner_nested_field """
+            select t.id
+            from test_correlated_dereference_outer t
+            where not exists (
+                select 1
+                from test_correlated_dereference_inner_scalar t
+                group by t.value
+                having count(*) > 1
+            )
+            order by t.id
+            """
+
+    test {
+        sql """
+            select t1.id
+            from test_correlated_dereference_outer t1
+            where exists (
+                select 1
+                from test_correlated_dereference_inner_scalar t1
+                where t1.`@event_name` = 'blocked'
+            )
+            """
+        exception "No such field '@event_name' in 't1'"
+    }
+}


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

Reply via email to