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]
