This is an automated email from the ASF dual-hosted git repository.
starocean999 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 ae4ed4d66ed [opt](merge into) Put MERGE INTO target on the probe side
(#66565)
ae4ed4d66ed is described below
commit ae4ed4d66ed45c541605d44a9b6e054f191394dc
Author: starocean999 <[email protected]>
AuthorDate: Mon Aug 10 09:31:29 2026 +0800
[opt](merge into) Put MERGE INTO target on the probe side (#66565)
Issue Number: close https://github.com/apache/doris/pull/66435
Related PR: https://github.com/apache/doris/pull/57044
Problem Summary:
Doris builds the hash table on the right child, so the structurally wide
side — the target must carry all columns plus the row identity (for
Iceberg: struct<file_path, row_position, ...> with a full S3 URI per
row) — always became the build side. Worse, LEFT_OUTER_JOIN is in
DENIED_JOIN_TYPES, so the merge join could never produce runtime filters
at all. The internal OLAP path (MergeIntoCommand) used LEFT_OUTER_JOIN
unconditionally, even without WHEN NOT MATCHED clauses (the external
path already had the INNER optimization). For reference, Trino plans
MERGE as target RIGHT JOIN source and only allows dynamic filters on
INNER || RIGHT — exactly complementary to Doris's denied list.
### 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 -->
---
.../commands/ExternalRowLevelMergePlanBuilder.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/ExternalRowLevelMergePlanBuilder.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExternalRowLevelMergePlanBuilder.java
index b3ac202bd4d..d2d43c20471 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExternalRowLevelMergePlanBuilder.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExternalRowLevelMergePlanBuilder.java
@@ -29,7 +29,6 @@ import org.apache.doris.nereids.analyzer.UnboundSlot;
import org.apache.doris.nereids.analyzer.UnboundStar;
import org.apache.doris.nereids.exceptions.AnalysisException;
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.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -42,13 +41,12 @@ import
org.apache.doris.nereids.trees.expressions.functions.scalar.If;
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.JoinType;
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.MergeOperation;
+import org.apache.doris.nereids.trees.plans.commands.merge.MergeUtils;
import
org.apache.doris.nereids.trees.plans.logical.LogicalExternalRowLevelMergeSink;
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;
@@ -125,13 +123,7 @@ public class ExternalRowLevelMergePlanBuilder {
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 7741e343a51..24b6adb0776 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
@@ -31,7 +31,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;
@@ -47,7 +46,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;
@@ -62,7 +60,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;
@@ -188,7 +185,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(
@@ -200,9 +197,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 1d61c6ac1f8..b2d9b32b708 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]