This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 10edba47178 branch-4.1:[opt](merge into) Put MERGE INTO target on the
probe side - #66565 (#67279)
10edba47178 is described below
commit 10edba47178b5e59275f5f072d6ca920b2d66cff
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]