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

morrySnow 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 d921bebcd90 [fix](agg) Normalize equivalent multi-column distinct 
counts (#65206)
d921bebcd90 is described below

commit d921bebcd90a3f55a2446a706d350f2624445c62
Author: feiniaofeiafei <[email protected]>
AuthorDate: Thu Jul 23 11:44:59 2026 +0800

    [fix](agg) Normalize equivalent multi-column distinct counts (#65206)
    
    ### What problem does this PR solve?
    
    Related PR: #54079
    
    Problem Summary: NormalizeAggregate treated reordered equivalent
    multi-column COUNT DISTINCT expressions as different aggregate
    functions, which could make the multi-distinct strategy construct a join
    from only one aggregate and fail with an index-out-of-bounds exception.
    It also retained duplicate arguments even though downstream COUNT
    DISTINCT processing treats them as a set, and the CountIf conversion
    indexed the original argument list using the deduplicated size.
    Canonicalize multi-column distinct arguments as an ordered set during
    normalization and make CountIf consume that deduplicated list directly.
    
    ### Release note
    
    Fix planning and execution of equivalent multi-column COUNT DISTINCT
    expressions with reordered or duplicate arguments.
---
 .../nereids/rules/analysis/NormalizeAggregate.java |  26 ++++-
 .../PushDownAggWithDistinctThroughJoinOneSide.java |   2 +-
 .../apache/doris/nereids/util/AggregateUtils.java  |  10 +-
 .../NormalizeMultiColumnDistinctCountTest.java     | 125 +++++++++++++++++++++
 .../agg_strategy/distinct_agg_rewriter.groovy      |  15 +--
 5 files changed, 162 insertions(+), 16 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/NormalizeAggregate.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/NormalizeAggregate.java
index aebe40c24d7..93a31c61f1a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/NormalizeAggregate.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/NormalizeAggregate.java
@@ -40,6 +40,7 @@ import 
org.apache.doris.nereids.trees.expressions.SubqueryExpr;
 import org.apache.doris.nereids.trees.expressions.WindowExpression;
 import 
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
 import org.apache.doris.nereids.trees.expressions.functions.agg.AnyValue;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
 import org.apache.doris.nereids.trees.expressions.functions.generator.Unnest;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
 import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
@@ -163,7 +164,8 @@ public class NormalizeAggregate implements 
RewriteRuleFactory, NormalizeToSlot {
         Set<Expression> groupingByExprs = 
Utils.fastToImmutableSet(aggregate.getGroupByExpressions());
 
         // collect all trivial-agg
-        List<NamedExpression> aggregateOutput = 
aggregate.getOutputExpressions();
+        List<NamedExpression> aggregateOutput = 
normalizeMultiColumnDistinctCount(
+                aggregate.getOutputExpressions());
         Map<AggregateFunction, Map<String, String>> aggFuncs =
                 
CollectNonWindowedAggFuncsWithSessionVar.collect(aggregateOutput);
 
@@ -448,6 +450,28 @@ public class NormalizeAggregate implements 
RewriteRuleFactory, NormalizeToSlot {
                 having.get().withChildren(new 
LogicalProject<>(bottomProjectsBuilder.build(), newAggregate)));
     }
 
+    private List<NamedExpression> 
normalizeMultiColumnDistinctCount(List<NamedExpression> aggregateOutput) {
+        // Multi-column distinct counts treat arguments as a set. Remove 
duplicates and canonicalize equivalent
+        // counts to the first argument order so the structural equality used 
below can share one aggregate result.
+        Map<ImmutableSet<Expression>, Count> distinctArgumentsToCount = new 
HashMap<>();
+        return ExpressionUtils.rewriteDownShortCircuit(aggregateOutput, 
expression -> {
+            if (!(expression instanceof Count)) {
+                return expression;
+            }
+            Count count = (Count) expression;
+            if (!count.isDistinct() || count.arity() <= 1) {
+                return count;
+            }
+            ImmutableSet<Expression> distinctArguments = 
ImmutableSet.copyOf(count.getDistinctArguments());
+            Count normalizedCount = 
distinctArgumentsToCount.get(distinctArguments);
+            if (normalizedCount == null) {
+                normalizedCount = count.withDistinctAndChildren(true, 
ImmutableList.copyOf(distinctArguments));
+                distinctArgumentsToCount.put(distinctArguments, 
normalizedCount);
+            }
+            return count.withDistinctAndChildren(true, 
normalizedCount.children());
+        });
+    }
+
     private List<NamedExpression> normalizeOutput(List<NamedExpression> 
aggregateOutput,
             NormalizeToSlotContext groupByToSlotContext, 
NormalizeToSlotContext argsOfAggFuncNeedPushDownContext,
             NormalizeToSlotContext normalizedAggFuncsToSlotContext) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownAggWithDistinctThroughJoinOneSide.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownAggWithDistinctThroughJoinOneSide.java
index 3f9ad609744..dd09b2b52a3 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownAggWithDistinctThroughJoinOneSide.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownAggWithDistinctThroughJoinOneSide.java
@@ -64,7 +64,7 @@ public class PushDownAggWithDistinctThroughJoinOneSide 
implements RewriteRuleFac
                             } else {
                                 return funcs.stream()
                                         .allMatch(f -> (f instanceof Min || f 
instanceof Max || f instanceof Sum
-                                                || f instanceof Count) && 
f.isDistinct()
+                                                || f instanceof Count) && 
f.isDistinct() && f.arity() == 1
                                                 && f.child(0) instanceof Slot);
                             }
                         })
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/AggregateUtils.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/AggregateUtils.java
index 947da096185..39ced0eddec 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/AggregateUtils.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/AggregateUtils.java
@@ -40,6 +40,7 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
@@ -62,10 +63,11 @@ public class AggregateUtils {
 
     /**countDistinctMultiExprToCountIf*/
     public static Expression countDistinctMultiExprToCountIf(Count count) {
-        Set<Expression> arguments = ImmutableSet.copyOf(count.getArguments());
-        Expression countExpr = count.getArgument(arguments.size() - 1);
-        for (int i = arguments.size() - 2; i >= 0; --i) {
-            Expression argument = count.getArgument(i);
+        Iterator<Expression> arguments = 
ImmutableSet.copyOf(count.getArguments())
+                .asList().reverse().iterator();
+        Expression countExpr = arguments.next();
+        while (arguments.hasNext()) {
+            Expression argument = arguments.next();
             If ifNull = new If(new IsNull(argument), NullLiteral.INSTANCE, 
countExpr);
             countExpr = assignNullType(ifNull);
         }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/NormalizeMultiColumnDistinctCountTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/NormalizeMultiColumnDistinctCountTest.java
new file mode 100644
index 00000000000..70bb1699949
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/NormalizeMultiColumnDistinctCountTest.java
@@ -0,0 +1,125 @@
+// 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.rules.analysis;
+
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.util.AggregateUtils;
+import org.apache.doris.nereids.util.MemoPatternMatchSupported;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.nereids.util.PlanConstructor;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class NormalizeMultiColumnDistinctCountTest implements 
MemoPatternMatchSupported {
+    @Test
+    public void testNormalizeDifferentArgumentOrder() {
+        Plan student = new 
LogicalOlapScan(StatementScopeIdGenerator.newRelationId(), 
PlanConstructor.student,
+                ImmutableList.of());
+        Slot id = student.getOutput().get(0);
+        Slot name = student.getOutput().get(2);
+        Alias countIdName = new Alias(new Count(true, id, name), 
"count_id_name");
+        Alias countNameId = new Alias(new Count(true, name, id), 
"count_name_id");
+        LogicalAggregate<Plan> root = new 
LogicalAggregate<>(ImmutableList.of(),
+                ImmutableList.of(countIdName, countNameId), student);
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), root)
+                .applyTopDown(new NormalizeAggregate())
+                .matchesFromRoot(
+                        logicalProject(
+                                logicalAggregate(
+                                        logicalProject(
+                                                logicalOlapScan()
+                                        )
+                                ).when(aggregate -> 
aggregate.getAggregateFunctions().size() == 1)
+                                        .when(aggregate -> 
aggregate.getOutputExpressions().size() == 1)
+                        ).when(project -> project.getProjects().size() == 2)
+                                .when(project -> 
project.getProjects().get(0).getExprId()
+                                        .equals(countIdName.getExprId()))
+                                .when(project -> 
project.getProjects().get(1).getExprId()
+                                        .equals(countNameId.getExprId()))
+                                .when(project -> 
project.getProjects().get(0).getInputSlots()
+                                        
.equals(project.getProjects().get(1).getInputSlots()))
+                );
+    }
+
+    @Test
+    public void testKeepDifferentDistinctArguments() {
+        Plan student = new 
LogicalOlapScan(StatementScopeIdGenerator.newRelationId(), 
PlanConstructor.student,
+                ImmutableList.of());
+        Slot id = student.getOutput().get(0);
+        Slot gender = student.getOutput().get(1);
+        Slot name = student.getOutput().get(2);
+        LogicalAggregate<Plan> root = new 
LogicalAggregate<>(ImmutableList.of(),
+                ImmutableList.of(
+                        new Alias(new Count(true, id, name), "count_id_name"),
+                        new Alias(new Count(true, id, gender), 
"count_id_gender")),
+                student);
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), root)
+                .applyTopDown(new NormalizeAggregate())
+                .matches(
+                        logicalJoin(
+                                logicalAggregate()
+                                        .when(aggregate -> 
aggregate.getAggregateFunctions().size() == 1),
+                                logicalAggregate()
+                                        .when(aggregate -> 
aggregate.getAggregateFunctions().size() == 1)
+                        )
+                );
+    }
+
+    @Test
+    public void testRemoveDuplicateDistinctArguments() {
+        Plan student = new 
LogicalOlapScan(StatementScopeIdGenerator.newRelationId(), 
PlanConstructor.student,
+                ImmutableList.of());
+        Slot id = student.getOutput().get(0);
+        Slot name = student.getOutput().get(2);
+        Count count = new Count(true, id, id, name);
+
+        Count countIf = (Count) 
AggregateUtils.countDistinctMultiExprToCountIf(count);
+        Assertions.assertEquals(ImmutableSet.of(id, name), 
countIf.getInputSlots());
+
+        LogicalAggregate<Plan> root = new 
LogicalAggregate<>(ImmutableList.of(),
+                ImmutableList.of(
+                        new Alias(count, "count_id_id_name"),
+                        new Alias(new Count(true, name, id), "count_name_id")),
+                student);
+        PlanChecker.from(MemoTestUtils.createConnectContext(), root)
+                .applyTopDown(new NormalizeAggregate())
+                .matchesFromRoot(
+                        logicalProject(
+                                logicalAggregate(
+                                        logicalProject(
+                                                logicalOlapScan()
+                                        )
+                                ).when(aggregate -> 
aggregate.getAggregateFunctions().size() == 1)
+                                        .when(aggregate -> 
aggregate.getAggregateFunctions().iterator().next()
+                                                .arity() == 2)
+                        )
+                );
+    }
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/agg_strategy/distinct_agg_rewriter.groovy
 
b/regression-test/suites/nereids_rules_p0/agg_strategy/distinct_agg_rewriter.groovy
index 33fb4f62db3..d3400db766f 100644
--- 
a/regression-test/suites/nereids_rules_p0/agg_strategy/distinct_agg_rewriter.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/agg_strategy/distinct_agg_rewriter.groovy
@@ -38,17 +38,12 @@ suite("distinct_agg_rewriter") {
 
     sql "set agg_phase=4"
     sql "select group_concat(distinct dst_key1 order by dst_key2) from 
t_gbykey_10_dstkey_10_1000_id group by gby_key"
-    test {
-        sql """select number % 2, count(distinct cast(number as varchar), 
cast(number as varchar)),
-        group_concat(distinct cast(number as varchar) order by number + 1) 
from numbers('number'='10') group by number % 2"""
-        exception "Unsupported query"
-    }
 
-    test {
-        sql """select count(distinct cast(number as varchar), cast(number as 
varchar)),
-        group_concat(distinct cast(number as varchar) order by number + 1) 
from numbers('number'='10')"""
-        exception "Unsupported query"
-    }
+    sql """select number % 2, count(distinct cast(number as varchar), 
cast(number as varchar)),
+    group_concat(distinct cast(number as varchar) order by number + 1) from 
numbers('number'='10') group by number % 2"""
+
+    sql """select count(distinct cast(number as varchar), cast(number as 
varchar)),
+    group_concat(distinct cast(number as varchar) order by number + 1) from 
numbers('number'='10')"""
 
     // expect to have 3 HashAgg instead of 4 HashAgg
     sql "set agg_phase=3"


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

Reply via email to