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 b3ac202bd4d330..d2d43c20471cc0 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.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.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 @@ private LogicalPlan generateBasePlan() { 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 7741e343a511e8..24b6adb07760ba 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.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.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.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 StmtType stmtType() { } /** - * 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 @@ private LogicalPlan generateBasePlan() { 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 00000000000000..1fbc4c84aea2a5 --- /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. + * + *

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 1d61c6ac1f8dd8..b2d9b32b708bcb 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 @@ source, new NullLiteral(), 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 nameParts = ImmutableList.of("ctl", "db", "tbl"); @@ -111,11 +136,11 @@ source, new NullLiteral(), 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()); }