This is an automated email from the ASF dual-hosted git repository.

englefly 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 f1cfc381d2b [fe](cse) Extract aggregate-argument CSE below distribute 
(#66815)
f1cfc381d2b is described below

commit f1cfc381d2bcc12540582c7c3669b3c930054804
Author: minghong <[email protected]>
AuthorDate: Tue Aug 18 22:25:39 2026 +0800

    [fe](cse) Extract aggregate-argument CSE below distribute (#66815)
    
    ### What problem does this PR solve?
    Extract aggregate-argument CSE below distribute
---
 .../post/ProjectAggregateExpressionsForCse.java    | 69 +++++++++++++++++++++-
 .../agg_strategy/cse_agg_distribute.out            |  5 ++
 .../agg_strategy/cse_agg_distribute.groovy         | 69 ++++++++++++++++++++++
 3 files changed, 141 insertions(+), 2 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/ProjectAggregateExpressionsForCse.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/ProjectAggregateExpressionsForCse.java
index a77640a55dc..91eb7c04b37 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/ProjectAggregateExpressionsForCse.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/ProjectAggregateExpressionsForCse.java
@@ -29,6 +29,8 @@ import 
org.apache.doris.nereids.trees.expressions.OrderExpression;
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
 import 
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
+import org.apache.doris.nereids.trees.plans.AggMode;
+import org.apache.doris.nereids.trees.plans.AggPhase;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.algebra.Aggregate;
 import org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalPlan;
@@ -64,12 +66,21 @@ public class ProjectAggregateExpressionsForCse extends 
PlanPostProcessor {
      * Shared CSE projection logic for PhysicalHashAggregate.
      * Extracts common sub-expressions from
      * aggregate function arguments into a project node beneath the aggregate.
+     *
+     * <p>For one-phase aggregates whose child is a PhysicalDistribute
+     * (aggregate -> distribute -> scan), the CSE project is inserted below the
+     * distribute so that the distribution-key slots stay intact and the 
exchange
+     * only carries the (already pruned) aggregate input. The translator's 
bucketed
+     * fusion (fusing one-phase aggregate + distribute into 
BucketedAggregationNode)
+     * builds directly on the distribute's child, so the fused plan naturally
+     * becomes BucketedAgg(sum(x), max(x)) -> Project(a+b AS x) -> scan and the
+     * common aggregate argument is evaluated once per row instead of once per
+     * aggregate function.</p>
      */
     private <T extends AbstractPhysicalPlan & Aggregate<? extends Plan>>
             Plan projectAggregateCse(T aggregate) {
         // For multi-phase aggregates, only process the 1st phase.
-        // Bucketed agg is always single-phase, but keep the same guard for 
safety.
-        if (aggregate.child() instanceof PhysicalDistribute || 
aggregate.child() instanceof Aggregate) {
+        if (aggregate.child() instanceof Aggregate) {
             return aggregate;
         }
 
@@ -161,6 +172,60 @@ public class ProjectAggregateExpressionsForCse extends 
PlanPostProcessor {
             project = 
project.withPhysicalPropertiesAndStats(projectPhysicalProperties, 
project.getStats());
             return (Plan) aggregate.withAggOutput(aggOutputReplaced)
                     .withChildren(project);
+        } else if (aggregate.child() instanceof PhysicalDistribute) {
+            // One-phase (INPUT_TO_RESULT) aggregate over a distribute
+            // (aggregate -> distribute -> scan): insert the CSE project 
between
+            // the distribute and its child, instead of between the aggregate 
and
+            // the distribute. This keeps the aggregate's child as a distribute
+            // (so bucketed fusion and the property machinery still see the 
same
+            // shape), and the project lands inside the scan
+            // fragment, so the common aggregate argument is computed once per 
row
+            // before the exchange. After bucketed fusion bypasses the 
distribute,
+            // the executed plan is BucketedAgg(sum(x), max(x)) -> Project(a+b 
AS x)
+            // -> scan.
+            //
+            // Only the one-phase shape reaches here with complex aggregate
+            // arguments: two-phase GLOBAL aggregates (BUFFER_TO_RESULT) 
reference
+            // the local phase's intermediate slots, so no CSE candidate is
+            // extracted for them anyway. Guard explicitly anyway to keep the
+            // intent clear and to stay safe if a future aggregate function
+            // surfaces a non-slot argument on the GLOBAL phase.
+            if (!(aggregate instanceof PhysicalHashAggregate)) {
+                return aggregate;
+            }
+            PhysicalHashAggregate<? extends Plan> hashAggregate =
+                    (PhysicalHashAggregate<? extends Plan>) aggregate;
+            if (hashAggregate.getAggPhase() != AggPhase.GLOBAL
+                    || hashAggregate.getAggMode() != AggMode.INPUT_TO_RESULT) {
+                return aggregate;
+            }
+            PhysicalDistribute<?> distribute = (PhysicalDistribute<?>) 
aggregate.child();
+            List<NamedExpression> projections = new ArrayList<>();
+            projections.addAll(inputSlots);
+            projections.addAll(cseCandidates.values());
+            List<Slot> projectOutput = new ImmutableList.Builder<Slot>()
+                    .addAll(inputSlots)
+                    .addAll(slotMap.values())
+                    .build();
+            LogicalProperties projectLogicalProperties = new LogicalProperties(
+                    () -> projectOutput,
+                    () -> DataTrait.EMPTY_TRAIT
+            );
+            AbstractPhysicalPlan distributeChild = ((AbstractPhysicalPlan) 
distribute.child());
+            PhysicalProperties projectPhysicalProperties = 
ChildOutputPropertyDeriver.computeProjectOutputProperties(
+                    projections, distributeChild.getPhysicalProperties());
+            PhysicalProject<? extends Plan> project = new 
PhysicalProject<>(projections, Optional.empty(),
+                    projectLogicalProperties,
+                    projectPhysicalProperties,
+                    distributeChild.getStats(),
+                    distribute.child());
+            // withChildren keeps the distribution spec and physical 
properties of the
+            // distribute unchanged; its output now comes from the CSE 
project, which
+            // still carries every distribution-key slot (the group-by slots 
are part
+            // of inputSlots above).
+            PhysicalDistribute<Plan> newDistribute = 
distribute.withChildren(ImmutableList.of(project));
+            return (Plan) aggregate.withAggOutput(aggOutputReplaced)
+                    .withChildren(newDistribute);
         } else {
             List<NamedExpression> projections = new ArrayList<>();
             projections.addAll(inputSlots);
diff --git 
a/regression-test/data/nereids_rules_p0/agg_strategy/cse_agg_distribute.out 
b/regression-test/data/nereids_rules_p0/agg_strategy/cse_agg_distribute.out
new file mode 100644
index 00000000000..91465208722
--- /dev/null
+++ b/regression-test/data/nereids_rules_p0/agg_strategy/cse_agg_distribute.out
@@ -0,0 +1,5 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !one_phase_join_result --
+g1     33      19      33      19
+g2     22      15      22      15
+
diff --git 
a/regression-test/suites/nereids_rules_p0/agg_strategy/cse_agg_distribute.groovy
 
b/regression-test/suites/nereids_rules_p0/agg_strategy/cse_agg_distribute.groovy
new file mode 100644
index 00000000000..86462e58e68
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/agg_strategy/cse_agg_distribute.groovy
@@ -0,0 +1,69 @@
+// 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.
+
+suite("cse_agg_distribute") {
+    sql "SET enable_nereids_planner=true"
+    sql "SET enable_fallback_to_original_planner=false"
+    sql "SET runtime_filter_mode=OFF"
+
+    sql "DROP TABLE IF EXISTS cse_agg_distribute_tbl"
+    sql """
+        CREATE TABLE cse_agg_distribute_tbl (
+            id int,
+            grp varchar(20),
+            a int,
+            b int
+        ) DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 3
+        PROPERTIES('replication_num' = '1')
+    """
+    sql """ INSERT INTO cse_agg_distribute_tbl VALUES
+        (1, 'g1', 1, 2),
+        (2, 'g2', 3, 4),
+        (3, 'g1', 5, 6),
+        (4, 'g2', 7, 8),
+        (5, 'g1', 9, 10)
+    """
+
+    // SUM(a+b) and MAX(a+b) share the same argument, so the aggregate-argument
+    // CSE must extract "a+b" into a project node and make both functions
+    // reference the extracted slot, instead of re-evaluating a+b per function.
+    String query = "SELECT grp, SUM(a+b), MAX(a+b) FROM cse_agg_distribute_tbl 
GROUP BY grp"
+
+    // ---------------------------------------------------------------------
+    // one-phase aggregate over a distribute (the aggregate is a join child,
+    // so the distribute is required by the join): the CSE project must be
+    // inserted below the distribute, keeping the distribution-key slots
+    // intact. Both aggregates must reference the extracted slot (4
+    // occurrences: SUM/MAX of each side).
+    // ---------------------------------------------------------------------
+    sql "set agg_phase=1"
+    sql "set enable_bucketed_hash_agg=false"
+    String joinQuery = """
+        SELECT t1.grp, t1.s, t1.m, t2.s2, t2.m2 FROM
+         (SELECT grp, SUM(a+b) s, MAX(a+b) m FROM cse_agg_distribute_tbl GROUP 
BY grp) t1
+         JOIN (SELECT grp, SUM(a+b) s2, MAX(a+b) m2 FROM 
cse_agg_distribute_tbl GROUP BY grp) t2
+         ON t1.grp = t2.grp
+    """
+    explain {
+        sql("${joinQuery}")
+        contains("VEXCHANGE")
+        contains("VSELECT")
+        multiContains("cast(a as BIGINT) + cast(b as BIGINT))[#", 4)
+    }
+    order_qt_one_phase_join_result """${joinQuery} ORDER BY t1.grp"""
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to