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

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git

commit 2416c8704153363bc62f4db983508145e801f7d6
Author: starocean999 <[email protected]>
AuthorDate: Thu Sep 10 09:23:18 2026 +0800

    branch-4.1:[opt](merge into) Put MERGE INTO target on the probe side - 
#66565 (#67279)
    
    pick https://github.com/apache/doris/pull/66565
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 .../trees/plans/commands/IcebergMergeCommand.java  | 12 +----
 .../plans/commands/merge/MergeIntoCommand.java     |  9 +---
 .../trees/plans/commands/merge/MergeUtils.java     | 58 ++++++++++++++++++++++
 .../plans/commands/merge/MergeIntoCommandTest.java | 43 ++++++++++++----
 4 files changed, 96 insertions(+), 26 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/IcebergMergeCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/IcebergMergeCommand.java
index be73ce25630..8692449b867 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/IcebergMergeCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/IcebergMergeCommand.java
@@ -39,7 +39,6 @@ import org.apache.doris.nereids.analyzer.UnboundStar;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.glue.LogicalPlanAdapter;
 import org.apache.doris.nereids.parser.LogicalPlanBuilderAssistant;
-import org.apache.doris.nereids.rules.exploration.join.JoinReorderContext;
 import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.Cast;
 import org.apache.doris.nereids.trees.expressions.DefaultValueSlot;
@@ -55,7 +54,6 @@ import 
org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
 import org.apache.doris.nereids.trees.plans.Explainable;
-import org.apache.doris.nereids.trees.plans.JoinType;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.PlanType;
 import 
org.apache.doris.nereids.trees.plans.commands.delete.DeleteCommandContext;
@@ -64,9 +62,9 @@ import 
org.apache.doris.nereids.trees.plans.commands.insert.IcebergMergeExecutor
 import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertCommandContext;
 import org.apache.doris.nereids.trees.plans.commands.merge.MergeMatchedClause;
 import 
org.apache.doris.nereids.trees.plans.commands.merge.MergeNotMatchedClause;
+import org.apache.doris.nereids.trees.plans.commands.merge.MergeUtils;
 import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
 import org.apache.doris.nereids.trees.plans.logical.LogicalIcebergMergeSink;
-import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias;
@@ -214,13 +212,7 @@ public class IcebergMergeCommand extends Command 
implements ForwardWithSync, Exp
         if (targetAlias.isPresent()) {
             targetPlan = new LogicalSubQueryAlias<>(targetAlias.get(), 
targetPlan);
         }
-        // Use INNER JOIN when there are no WHEN NOT MATCHED clauses, since 
unmatched
-        // source rows are not needed. This allows early filtering for better 
performance.
-        JoinType joinType = notMatchedClauses.isEmpty()
-                ? JoinType.INNER_JOIN : JoinType.LEFT_OUTER_JOIN;
-        return new LogicalJoin<>(joinType,
-                ImmutableList.of(), ImmutableList.of(onClause),
-                source, targetPlan, JoinReorderContext.EMPTY);
+        return MergeUtils.buildMergeJoin(targetPlan, source, onClause, 
!notMatchedClauses.isEmpty());
     }
 
     private NamedExpression generateBranchLabel(Expression rowIdExpr) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
index 6de86836fb4..15c382306a1 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
@@ -33,7 +33,6 @@ import 
org.apache.doris.nereids.analyzer.UnboundTableSinkCreator;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.parser.LogicalPlanBuilderAssistant;
 import org.apache.doris.nereids.parser.NereidsParser;
-import org.apache.doris.nereids.rules.exploration.join.JoinReorderContext;
 import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.Cast;
 import org.apache.doris.nereids.trees.expressions.DefaultValueSlot;
@@ -49,7 +48,6 @@ import 
org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
 import org.apache.doris.nereids.trees.plans.Explainable;
-import org.apache.doris.nereids.trees.plans.JoinType;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.PlanType;
 import org.apache.doris.nereids.trees.plans.commands.Command;
@@ -60,7 +58,6 @@ import 
org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
 import org.apache.doris.nereids.trees.plans.commands.info.DMLCommandType;
 import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTableCommand;
 import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
-import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias;
@@ -186,7 +183,7 @@ public class MergeIntoCommand extends Command implements 
ForwardWithSync, Explai
     }
 
     /**
-     * generate target right outer join source.
+     * generate target (inner | right outer) join source, see {@link 
MergeUtils#buildMergeJoin}.
      */
     private LogicalPlan generateBasePlan() {
         LogicalPlan plan = LogicalPlanBuilderAssistant.withCheckPolicy(
@@ -198,9 +195,7 @@ public class MergeIntoCommand extends Command implements 
ForwardWithSync, Explai
         if (targetAlias.isPresent()) {
             plan = new LogicalSubQueryAlias<>(targetAlias.get(), plan);
         }
-        return new LogicalJoin<>(JoinType.LEFT_OUTER_JOIN,
-                ImmutableList.of(), ImmutableList.of(onClause),
-                source, plan, JoinReorderContext.EMPTY);
+        return MergeUtils.buildMergeJoin(plan, source, onClause, 
!notMatchedClauses.isEmpty());
     }
 
     /**
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeUtils.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeUtils.java
new file mode 100644
index 00000000000..1fbc4c84aea
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeUtils.java
@@ -0,0 +1,58 @@
+// 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.trees.plans.commands.merge;
+
+import org.apache.doris.nereids.rules.exploration.join.JoinReorderContext;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Shared plan-construction helpers for MERGE INTO, used by both the internal 
OLAP path
+ * ({@link MergeIntoCommand}) and the external path
+ * ({@link 
org.apache.doris.nereids.trees.plans.commands.ExternalRowLevelMergePlanBuilder}).
+ */
+public class MergeUtils {
+
+    private MergeUtils() {
+    }
+
+    /**
+     * Build the base join between merge target and source, with the target on 
the LEFT (probe)
+     * side. Doris builds the hash table on the right child, and the target 
side is structurally
+     * the wide one: it must carry every table column plus the row identity 
for the sink, while
+     * the source usually only carries join keys and new values. Keeping the 
target on the probe
+     * side also lets RuntimeFilterGenerator prune the target scan with 
runtime filters built
+     * from the source side: INNER and RIGHT_OUTER joins may produce runtime 
filters while
+     * LEFT_OUTER is in its denied list.
+     *
+     * <p>Unmatched source rows are only needed by WHEN NOT MATCHED clauses, 
so without them the
+     * join is INNER; with them, RIGHT OUTER preserves exactly the unmatched 
source rows, which
+     * is equivalent to the previous "source LEFT OUTER JOIN target" shape.
+     */
+    public static LogicalPlan buildMergeJoin(LogicalPlan targetPlan, 
LogicalPlan source,
+            Expression onClause, boolean hasNotMatchedClauses) {
+        JoinType joinType = hasNotMatchedClauses ? JoinType.RIGHT_OUTER_JOIN : 
JoinType.INNER_JOIN;
+        return new LogicalJoin<>(joinType,
+                ImmutableList.of(), ImmutableList.of(onClause),
+                targetPlan, source, JoinReorderContext.EMPTY);
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommandTest.java
index 2023e14bdcb..e51382332f4 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommandTest.java
@@ -85,13 +85,38 @@ public class MergeIntoCommandTest {
         Assertions.assertEquals(1, logicalJoin.getOtherJoinConjuncts().size());
         Expression onClause = logicalJoin.getOtherJoinConjuncts().get(0);
         Assertions.assertEquals(new NullLiteral(), onClause);
-        Assertions.assertEquals(JoinType.LEFT_OUTER_JOIN, 
logicalJoin.getJoinType());
-        Assertions.assertEquals(source, logicalJoin.left());
-        Assertions.assertInstanceOf(LogicalSubQueryAlias.class, 
logicalJoin.right());
-        LogicalSubQueryAlias<?> alias = (LogicalSubQueryAlias<?>) 
logicalJoin.right();
+        // without WHEN NOT MATCHED clauses unmatched source rows are not 
needed, so the join
+        // is INNER, and the target stays on the left (probe) side
+        Assertions.assertEquals(JoinType.INNER_JOIN, 
logicalJoin.getJoinType());
+        Assertions.assertEquals(source, logicalJoin.right());
+        Assertions.assertInstanceOf(LogicalSubQueryAlias.class, 
logicalJoin.left());
+        LogicalSubQueryAlias<?> alias = (LogicalSubQueryAlias<?>) 
logicalJoin.left();
         Assertions.assertEquals("alias", alias.getAlias());
     }
 
+    @Test
+    public void testGenerateBasePlanWithNotMatchedClause() throws Exception {
+        LogicalPlan source = new LogicalEmptyRelation(new RelationId(1), 
ImmutableList.of());
+        MergeIntoCommand command = new MergeIntoCommand(
+                ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"), 
Optional.empty(),
+                source, new NullLiteral(),
+                ImmutableList.of(),
+                ImmutableList.of(new MergeNotMatchedClause(
+                        Optional.empty(), ImmutableList.of(), 
ImmutableList.of()))
+        );
+
+        Class<?> clazz = 
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+        Method generateBasePlan = clazz.getDeclaredMethod("generateBasePlan");
+        generateBasePlan.setAccessible(true);
+        LogicalPlan result = (LogicalPlan) generateBasePlan.invoke(command);
+        Assertions.assertInstanceOf(LogicalJoin.class, result);
+        LogicalJoin<?, ?> logicalJoin = (LogicalJoin<?, ?>) result;
+        // WHEN NOT MATCHED needs the unmatched source rows: source is the 
preserved right side
+        Assertions.assertEquals(JoinType.RIGHT_OUTER_JOIN, 
logicalJoin.getJoinType());
+        Assertions.assertEquals(source, logicalJoin.right());
+        Assertions.assertInstanceOf(LogicalSubQueryAlias.class, 
logicalJoin.left());
+    }
+
     @Test
     public void testGenerateBasePlanWithoutAlias() throws Exception {
         List<String> nameParts = ImmutableList.of("ctl", "db", "tbl");
@@ -111,11 +136,11 @@ public class MergeIntoCommandTest {
         Assertions.assertEquals(1, logicalJoin.getOtherJoinConjuncts().size());
         Expression onClause = logicalJoin.getOtherJoinConjuncts().get(0);
         Assertions.assertEquals(new NullLiteral(), onClause);
-        Assertions.assertEquals(JoinType.LEFT_OUTER_JOIN, 
logicalJoin.getJoinType());
-        Assertions.assertEquals(source, logicalJoin.left());
-        Assertions.assertInstanceOf(LogicalCheckPolicy.class, 
logicalJoin.right());
-        Assertions.assertInstanceOf(UnboundRelation.class, 
logicalJoin.right().child(0));
-        UnboundRelation unboundRelation = (UnboundRelation) 
logicalJoin.right().child(0);
+        Assertions.assertEquals(JoinType.INNER_JOIN, 
logicalJoin.getJoinType());
+        Assertions.assertEquals(source, logicalJoin.right());
+        Assertions.assertInstanceOf(LogicalCheckPolicy.class, 
logicalJoin.left());
+        Assertions.assertInstanceOf(UnboundRelation.class, 
logicalJoin.left().child(0));
+        UnboundRelation unboundRelation = (UnboundRelation) 
logicalJoin.left().child(0);
         Assertions.assertEquals(nameParts, unboundRelation.getNameParts());
     }
 


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

Reply via email to