This is an automated email from the ASF dual-hosted git repository. HappenLee pushed a commit to branch 4.1_performance in repository https://gitbox.apache.org/repos/asf/doris.git
commit 541bdd1a35a546b2c7c76333070430459ffc21a6 Author: minghong <[email protected]> AuthorDate: Mon Aug 3 19:00:38 2026 +0800 (14_1)PushDownJoinOnAssertNumRows support hashJoin (#66384) ### What problem does this PR solve? Issue Number: close #xxx 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 --> --- .../rules/rewrite/PushDownJoinOnAssertNumRows.java | 26 +++- .../rewrite/PushDownJoinOnAssertNumRowsTest.java | 163 +++++++++++++++++++++ 2 files changed, 184 insertions(+), 5 deletions(-) diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOnAssertNumRows.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOnAssertNumRows.java index d45cc5676fe..afc2f281bb1 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOnAssertNumRows.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOnAssertNumRows.java @@ -74,6 +74,22 @@ import java.util.stream.Collectors; * `-- bottomJoin(T2.a > x) * |-- Scan(T2) * `-- LogicalAssertNumRows(output=(x, ...)) + * + * Case 3: Push down with equi (hash) condition, e.g. scalar subquery + * select * from T1 join T2 where T1.b=T2.b and T2.c = (select x from T3 ...) + * Before: + * topJoin(T2.c = x) + * |-- bottomJoin(T1.b = T2.b) + * | |-- Scan(T1) + * | `-- Scan(T2) + * `-- LogicalAssertNumRows(output=(x, ...)) + * + * After: + * bottomJoin(T1.b = T2.b) + * |-- Scan(T1) + * `-- topJoin(T2.c = x) + * |-- Scan(T2) + * `-- LogicalAssertNumRows(output=(x, ...)) * </pre> */ public class PushDownJoinOnAssertNumRows extends OneRewriteRuleFactory { @@ -114,10 +130,8 @@ public class PushDownJoinOnAssertNumRows extends OneRewriteRuleFactory { return false; } - if (topJoin.getHashJoinConjuncts().isEmpty()) { - return topJoin.getOtherJoinConjuncts().size() == 1; - } - return false; + // only one join condition, either hash (equi) or other (non-equi) conjunct. + return topJoin.getHashJoinConjuncts().size() + topJoin.getOtherJoinConjuncts().size() == 1; } private boolean isAssertOneRowEqOrProjectAssertOneRowEq(Plan plan) { @@ -141,7 +155,9 @@ public class PushDownJoinOnAssertNumRows extends OneRewriteRuleFactory { private Plan pushDownAssertNumRowsJoin(LogicalJoin<?, ?> topJoin) { Plan assertBranch = topJoin.right(); - Expression condition = topJoin.getOtherJoinConjuncts().get(0); + Expression condition = topJoin.getHashJoinConjuncts().isEmpty() + ? topJoin.getOtherJoinConjuncts().get(0) + : topJoin.getHashJoinConjuncts().get(0); List<Alias> aliasUsedInConditionFromLeftProject = new ArrayList<>(); LogicalJoin<? extends Plan, ? extends Plan> bottomJoin; if (topJoin.left() instanceof LogicalProject) { diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOnAssertNumRowsTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOnAssertNumRowsTest.java index d241433a219..15f9292dfd0 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOnAssertNumRowsTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOnAssertNumRowsTest.java @@ -319,6 +319,169 @@ class PushDownJoinOnAssertNumRowsTest implements MemoPatternMatchSupported { logicalAssertNumRows())))); } + /** + * Test push down to right child with equi (hash) condition on the top join: + * Before: + * topJoin(T2.score = x) + * |-- bottomJoin(T1.id = T2.sid) + * | |-- Scan(T1) + * | `-- Scan(T2) + * `-- LogicalAssertNumRows(output=(x, ...)) + * + * After: + * bottomJoin(T1.id = T2.sid) + * |-- Scan(T1) + * `-- topJoin(T2.score = x) + * |-- Scan(T2) + * `-- LogicalAssertNumRows(output=(x, ...)) + */ + @Test + void testPushDownToRightChildWithHashConjunct() { + Plan oneRowRelation = new LogicalPlanBuilder(t3) + .limit(1) + .build(); + + AssertNumRowsElement assertElement = new AssertNumRowsElement(1, "", Assertion.EQ); + LogicalAssertNumRows<Plan> assertNumRows = new LogicalAssertNumRows<>(assertElement, oneRowRelation); + + // Create bottom join: T1 JOIN T2 on T1.id = T2.sid + Expression bottomJoinCondition = new EqualTo(t1Slots.get(0), t2Slots.get(1)); + + LogicalPlan bottomJoin = new LogicalPlanBuilder(t1) + .join(t2, JoinType.INNER_JOIN, ImmutableList.of(bottomJoinCondition), + ImmutableList.of()) + .build(); + + // Create top join: (T1 JOIN T2) JOIN assertNumRows on T2.score = course.name + // This references T2 (right child of bottom join) and assertNumRows + Expression topJoinCondition = new EqualTo(t2Slots.get(2), t3Slots.get(1)); + + LogicalPlan root = new LogicalPlanBuilder(bottomJoin) + .join(assertNumRows, JoinType.INNER_JOIN, ImmutableList.of(topJoinCondition), + ImmutableList.of()) + .build(); + + // Apply the rule + PlanChecker.from(MemoTestUtils.createConnectContext(), root) + .applyTopDown(new PushDownJoinOnAssertNumRows()) + .matches(logicalJoin( + logicalOlapScan(), + logicalJoin( + logicalOlapScan(), + logicalAssertNumRows()))); + } + + /** + * Test push down to right child with equi (hash) condition and a project + * between the top join and the bottom join, mirroring the plan shape of + * TPC-DS Q58: store_sales join date_dim, plus a scalar subquery on date_dim: + * Before: + * topJoin(d_week_seq = x) + * |-- Project(all output of bottomJoin) + * | `-- bottomJoin(ss_sold_date_sk = d_date_sk) + * | |-- Scan(T1) + * | `-- Scan(T2) + * `-- LogicalAssertNumRows(output=(x, ...)) + * + * After: + * Project(all output) + * `-- bottomJoin(ss_sold_date_sk = d_date_sk) + * |-- Scan(T1) + * `-- topJoin(d_week_seq = x) + * |-- Scan(T2) + * `-- LogicalAssertNumRows(output=(x, ...)) + */ + @Test + void testPushDownToRightChildWithProjectAndHashConjunct() { + Plan oneRowRelation = new LogicalPlanBuilder(t3) + .limit(1) + .build(); + + AssertNumRowsElement assertElement = new AssertNumRowsElement(1, "", Assertion.EQ); + LogicalAssertNumRows<Plan> assertNumRows = new LogicalAssertNumRows<>(assertElement, oneRowRelation); + + // Create bottom join: T1 JOIN T2 on T1.id = T2.sid + Expression bottomJoinCondition = new EqualTo(t1Slots.get(0), t2Slots.get(1)); + + LogicalPlan bottomJoin = new LogicalPlanBuilder(t1) + .join(t2, JoinType.INNER_JOIN, ImmutableList.of(bottomJoinCondition), + ImmutableList.of()) + .build(); + + // Wrap the bottom join in a project that passes through all its output. + LogicalProject<Plan> project = new LogicalProject<>(ImmutableList.copyOf(bottomJoin.getOutput()), bottomJoin); + + // Create top join: project JOIN assertNumRows on T2.score = course.name + Expression topJoinCondition = new EqualTo(t2Slots.get(2), t3Slots.get(1)); + + LogicalPlan root = new LogicalPlanBuilder(project) + .join(assertNumRows, JoinType.INNER_JOIN, ImmutableList.of(topJoinCondition), + ImmutableList.of()) + .build(); + + // Apply the rule + PlanChecker.from(MemoTestUtils.createConnectContext(), root) + .applyTopDown(new PushDownJoinOnAssertNumRows()) + .matches(logicalProject( + logicalJoin( + logicalOlapScan(), + logicalJoin( + logicalOlapScan(), + logicalAssertNumRows())))); + } + + /** + * Test push down to left child with equi (hash) condition on the top join: + * Before: + * topJoin(T1.age = x) + * |-- bottomJoin(T1.id = T2.sid) + * | |-- Scan(T1) + * | `-- Scan(T2) + * `-- LogicalAssertNumRows(output=(x, ...)) + * + * After: + * bottomJoin(T1.id = T2.sid) + * |-- topJoin(T1.age = x) + * | |-- Scan(T1) + * | `-- LogicalAssertNumRows(output=(x, ...)) + * `-- Scan(T2) + */ + @Test + void testPushDownToLeftChildWithHashConjunct() { + Plan oneRowRelation = new LogicalPlanBuilder(t3) + .limit(1) + .build(); + + AssertNumRowsElement assertElement = new AssertNumRowsElement(1, "", Assertion.EQ); + LogicalAssertNumRows<Plan> assertNumRows = new LogicalAssertNumRows<>(assertElement, oneRowRelation); + + // Create bottom join: T1 JOIN T2 on T1.id = T2.sid + Expression bottomJoinCondition = new EqualTo(t1Slots.get(0), t2Slots.get(1)); + + LogicalPlan bottomJoin = new LogicalPlanBuilder(t1) + .join(t2, JoinType.INNER_JOIN, ImmutableList.of(bottomJoinCondition), + ImmutableList.of()) + .build(); + + // Create top join: (T1 JOIN T2) JOIN assertNumRows on T1.age = course.id + // This references T1 (left child of bottom join) and assertNumRows + Expression topJoinCondition = new EqualTo(t1Slots.get(1), t3Slots.get(0)); + + LogicalPlan root = new LogicalPlanBuilder(bottomJoin) + .join(assertNumRows, JoinType.INNER_JOIN, ImmutableList.of(topJoinCondition), + ImmutableList.of()) + .build(); + + // Apply the rule + PlanChecker.from(MemoTestUtils.createConnectContext(), root) + .applyTopDown(new PushDownJoinOnAssertNumRows()) + .matches(logicalJoin( + logicalJoin( + logicalOlapScan(), + logicalAssertNumRows()), + logicalOlapScan())); + } + /** * Test with CROSS JOIN type. */ --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
