github-actions[bot] commented on code in PR #67884:
URL: https://github.com/apache/doris/pull/67884#discussion_r4079002715


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SimplifyAggGroupBy.java:
##########
@@ -20,117 +20,99 @@
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
 import org.apache.doris.nereids.trees.expressions.Add;
-import org.apache.doris.nereids.trees.expressions.BinaryArithmetic;
 import org.apache.doris.nereids.trees.expressions.Cast;
-import org.apache.doris.nereids.trees.expressions.Divide;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.Multiply;
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.Subtract;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Abs;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.IsInf;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.IsNan;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
-import org.apache.doris.nereids.util.ExpressionUtils;
-import org.apache.doris.nereids.util.Utils;
+import org.apache.doris.nereids.types.DataType;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 
 /**
- * Simplify Aggregate group by Multiple to One. For example
+ * Remove deterministic grouping expressions whose inputs are already bare 
grouping slots.
  * <p>
- * GROUP BY ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3
+ * GROUP BY ClientIP, ClientIP + 1, ClientIP + 2
  * -->
  * GROUP BY ClientIP
+ *
+ * <p>Retain existing bare slots so aggregate outputs can still reference 
them. A cast group key
+ * cannot generally provide its original slot to those outputs. Never 
synthesize a slot from
+ * derived keys, since doing so can split groups formed by non-injective 
expressions.</p>
  */
 public class SimplifyAggGroupBy extends OneRewriteRuleFactory {
-    private static final ImmutableSet<Class<? extends Expression>> 
supportedFunctions
-            = ImmutableSet.of(Add.class, Subtract.class, Multiply.class, 
Divide.class);
-
     @Override
     public Rule build() {
         return logicalAggregate()
-                .when(agg -> agg.getGroupByExpressions().size() > 1
-                        && 
ExpressionUtils.allMatch(agg.getGroupByExpressions(),
-                        SimplifyAggGroupBy::isBinaryArithmeticSlot))
+                .when(agg -> agg.getGroupByExpressions().size() > 1)
                 .then(agg -> {
-                    List<Expression> groupByExpressions = 
agg.getGroupByExpressions();
-                    ImmutableSet.Builder<Expression> inputSlots
-                            = 
ImmutableSet.builderWithExpectedSize(groupByExpressions.size());
-                    for (Expression groupByExpression : groupByExpressions) {
-                        inputSlots.addAll(groupByExpression.getInputSlots());
-                    }
-                    Set<Expression> slots = inputSlots.build();
-                    if (slots.size() != 1) {
+                    List<Expression> simplified = 
simplifyGroupBy(agg.getGroupByExpressions());
+                    if (simplified == null) {
                         return null;
                     }
-                    return 
agg.withGroupByAndOutput(Utils.fastToImmutableList(slots), 
agg.getOutputExpressions());
+                    return agg.withGroupByAndOutput(simplified, 
agg.getOutputExpressions());
                 })
                 .toRule(RuleType.SIMPLIFY_AGG_GROUP_BY);
     }
 
     @VisibleForTesting
-    protected static boolean isBinaryArithmeticSlot(Expression expr) {
-        if (expr instanceof Slot) {
-            return true;
-        }
-        if (!(expr instanceof BinaryArithmetic)) {
-            return false;
-        }
-        if (!supportedFunctions.contains(expr.getClass())) {
-            return false;
+    protected static List<Expression> simplifyGroupBy(List<Expression> 
groupByExpressions) {
+        Set<Expression> distinctGroupBy = new 
LinkedHashSet<>(groupByExpressions);
+        Set<Expression> determinants = distinctGroupBy.stream()
+                
.filter(Slot.class::isInstance).collect(ImmutableSet.toImmutableSet());
+        // Keep at least one key: removing all constant keys changes the 
result for empty input.
+        if (!determinants.isEmpty()) {
+            distinctGroupBy.removeIf(expression -> !(expression instanceof 
Slot)
+                    && !expression.containsVolatileOrNoneMovableExpression()
+                    && !expression.containsNondeterministic()
+                    && determinants.containsAll(expression.getInputSlots())
+                    && preservesGroupingEquality(expression));
         }
+        return distinctGroupBy.size() == groupByExpressions.size() ? null : 
ImmutableList.copyOf(distinctGroupBy);
+    }
 
-        // Float/double arithmetic: precision loss for all operations
-        if (expr.child(0).getDataType().isFloatLikeType()
-                || expr.child(1).getDataType().isFloatLikeType()) {
+    /**
+     * Doris grouping equality merges signed zeros and NaN payloads. A 
dependent expression
+     * can be removed only if it maps each such equivalence class to one 
grouping result.
+     * Addition, subtraction, multiplication, abs, floating casts, isnan, and 
isinf
+     * preserve those classes. Signbit, atan2, pow, and string casts can 
distinguish
+     * their members, so unreviewed float-dependent operations stay.
+     */
+    private static boolean preservesGroupingEquality(Expression expression) {
+        Set<Slot> inputSlots = expression.getInputSlots();
+        if (inputSlots.stream().anyMatch(slot -> 
!hasExactGroupingEquality(slot)
+                && !slot.getDataType().isFloatLikeType())) {
             return false;
         }
-
-        Expression slotExpr;
-        Literal literal;
-        if (expr.child(0) instanceof Literal) {
-            literal = (Literal) expr.child(0);
-            slotExpr = expr.child(1);
-        } else if (expr.child(1) instanceof Literal) {
-            literal = (Literal) expr.child(1);
-            slotExpr = expr.child(0);
-        } else {
-            return false;
+        if (inputSlots.stream().noneMatch(slot -> 
slot.getDataType().isFloatLikeType())) {
+            return true;
         }
-
-        if (!canExtractSlot(slotExpr)) {
-            return false;
+        if (expression instanceof Slot || expression instanceof Literal) {
+            return true;
         }
-
-        return checkLiteral((BinaryArithmetic) expr, literal);
-    }
-
-    @VisibleForTesting
-    protected static boolean checkLiteral(BinaryArithmetic expr, Literal 
literal) {
-        if (literal.isNullLiteral()) {
+        if (expression instanceof Cast && 
!expression.getDataType().isFloatLikeType()) {
             return false;
         }
-        if (expr instanceof Multiply || expr instanceof Divide) {
-            if (literal.isZero()) {
-                return false;
-            }
+        if (!(expression instanceof Cast || expression instanceof Add || 
expression instanceof Subtract
+                || expression instanceof Multiply || expression instanceof Abs

Review Comment:
   [P1] Preserve the canonicalized value of removed float keys
   
   With `ELIMINATE_GROUP_BY_KEY` disabled, consider `SELECT signbit(v * -1.0), 
count(*) FROM t GROUP BY v, v * -1.0`. Before this change the normalized tree 
is:
   
   ```text
   Project(signbit(k), count)
     Aggregate(keys=[v, k])
       Project(v, v * -1.0 AS k)
   ```
   
   Every BE aggregation sink calls `replace_float_special_values()` on each 
key, so `k = -0` is canonicalized to `+0` and `signbit(k)` is `false`. 
Admitting `Multiply` here removes `k`; `NormalizeAggregate` instead builds 
`Project(signbit(v * -1.0)) -> Aggregate(keys=[v])`. The aggregate returns 
canonical `v = +0`, then the upper project recomputes `-0`, changing the result 
to `true` even though the group count is unchanged. The new count-only 
safe-arithmetic regression cannot catch this. Please retain float-derived keys 
whose canonicalized value is consumed by aggregate outputs (directly or as a 
subtree), or otherwise preserve that canonicalized value, and add an 
output-sensitive regression.



##########
regression-test/suites/nereids_rules_p0/simplify_agg_group_by/simplify_agg_group_by.groovy:
##########
@@ -0,0 +1,268 @@
+// 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("simplify_agg_group_by") {
+    sql "drop table if exists simplify_agg_group_by_decimal"
+    sql """
+        create table simplify_agg_group_by_decimal (
+            x int not null,
+            y int not null,
+            d decimal(38, 0) not null
+        )
+        duplicate key(x)
+        distributed by hash(x) buckets 1
+        properties("replication_num" = "1")
+    """
+    sql """
+        insert into simplify_agg_group_by_decimal values
+            (1, 10, 99999999999999999999999999999999999999),
+            (2, 20, 1)
+    """
+
+    order_qt_decimal_dependency """
+        select count(*)
+        from simplify_agg_group_by_decimal
+        group by d, d * 10
+    """
+
+    order_qt_decimal_alias_dependency """
+        select count(*)
+        from (select d, d * 10 as risky from simplify_agg_group_by_decimal) t
+        group by d, risky
+    """
+
+    explain {
+        sql """
+            select count(*)
+            from simplify_agg_group_by_decimal
+            group by x, x + 1, x + 2
+        """
+        contains "group by: x[#"
+        notContains " + 1)"
+        notContains " + 2)"
+    }
+
+    explain {
+        sql """
+            select count(*)
+            from simplify_agg_group_by_decimal
+            group by x + 1, x + 2
+        """
+        contains "(cast(x as BIGINT) + 1)"
+        contains "(cast(x as BIGINT) + 2)"
+        notContains "group by: x[#"
+    }
+
+    explain {
+        sql """
+            select count(*)
+            from simplify_agg_group_by_decimal
+            group by x, x + 1, y, y + 1
+        """
+        contains "group by: x[#"
+        contains "y[#"
+        notContains " + 1)"
+    }
+
+    explain {
+        sql """
+            select count(*)
+            from simplify_agg_group_by_decimal
+            group by cast(x as bigint), x + 1
+        """
+        contains "cast(x as BIGINT)"
+        contains " + 1)"
+    }
+
+    explain {
+        sql """
+            select count(*)
+            from simplify_agg_group_by_decimal
+            group by x, y, x + y, abs(x)
+        """
+        contains "group by: x[#"
+        contains "y[#"
+        notContains " + "
+        notContains "abs("
+    }
+
+    order_qt_multiple_input_dependency """
+        select x + y, abs(x), count(*)
+        from simplify_agg_group_by_decimal
+        group by x, y, x + y, abs(x)
+    """
+
+    order_qt_constant_grouping_empty_input """
+        select count(*)
+        from simplify_agg_group_by_decimal
+        where x < 0
+        group by 'constant', 2 + 3
+    """
+
+    order_qt_existing_slot_determinant """
+        select x, count(*)
+        from simplify_agg_group_by_decimal
+        group by x, x + 1, x + 2
+        order by x
+    """
+
+    order_qt_multiple_determinants """
+        select x, y, count(*)
+        from simplify_agg_group_by_decimal
+        group by x, x + 1, y, y + 1
+        order by x, y
+    """
+
+    order_qt_injective_cast_and_dependent_outputs """
+        select cast(x as bigint), x + 1, count(*)
+        from simplify_agg_group_by_decimal
+        group by cast(x as bigint), x + 1
+        order by cast(x as bigint), x + 1
+    """
+
+    explain {
+        sql """
+            select cast(x as bigint), x + 1, count(*)
+            from simplify_agg_group_by_decimal
+            group by cast(x as bigint), x + 1
+        """
+        contains "group by: cast(x as"
+        contains ", x + 1[#"
+    }
+
+    order_qt_injective_cast_dependent_output """
+        select x + 1, count(*)
+        from simplify_agg_group_by_decimal
+        group by cast(x as bigint), x + 1
+        order by x + 1
+    """
+
+    explain {
+        sql """
+            select x + 1, count(*)
+            from simplify_agg_group_by_decimal
+            group by cast(x as bigint), x + 1
+        """
+        contains "group by: cast(x as"
+        contains ", x + 1[#"
+    }
+
+    explain {
+        sql """
+            select count(*) as n
+            from simplify_agg_group_by_decimal
+            group by x / 1000000.0, x / 2000000.0
+            order by n
+        """
+        contains "(cast(x as DECIMALV3(15, 5)) / 1000000.0)"
+        contains "(cast(x as DECIMALV3(15, 5)) / 2000000.0)"
+        notContains "group by: x[#"
+    }
+
+    order_qt_decimal_group_key_collision """
+        select count(*) as n
+        from simplify_agg_group_by_decimal
+        group by x / 1000000.0, x / 2000000.0
+        order by n
+    """
+    order_qt_try_cast_dependency """
+        select try_cast(d as bigint) as k, count(*)
+        from simplify_agg_group_by_decimal
+        group by d, try_cast(d as bigint)
+        order by k
+    """
+
+    explain {
+        sql """
+            select count(*)
+            from simplify_agg_group_by_decimal
+            group by x, cast(x as string)
+        """
+        contains "group by: x[#"
+        notContains "cast(x as TEXT)"
+    }
+
+    // Non-movable grouping functions must still run, even if a bare slot 
determines them.
+    test {
+        sql """
+            select count(*)
+            from simplify_agg_group_by_decimal
+            group by x, assert_true(x > 1, 'bad')
+        """
+        exception "bad"
+    }
+
+    // score() is rejected by a later validator; simplification must not erase 
it first.
+    test {
+        sql """
+            select count(*)
+            from simplify_agg_group_by_decimal
+            group by x, score()
+        """
+        exception "score() function requires WHERE clause with MATCH function"
+    }
+
+    sql "drop table if exists simplify_agg_group_by_signed_zero"
+    sql """
+        create table simplify_agg_group_by_signed_zero (
+            id int not null,
+            v double not null
+        )
+        duplicate key(id)
+        distributed by hash(id) buckets 1
+        properties("replication_num" = "1")
+    """
+    sql "insert into simplify_agg_group_by_signed_zero values (1, cast('+0.0' 
as double)), (2, cast('-0.0' as double))"
+    sql "set disable_nereids_rules='ELIMINATE_GROUP_BY_KEY'"

Review Comment:
   [P1] Exercise the default FD rewrite in these regressions
   
   Disabling `ELIMINATE_GROUP_BY_KEY` masks a second production path that 
bypasses the new guard. With the default rules, normalization produces:
   
   ```text
   Project(s, count)
     Aggregate(keys=[v, s])
       Project(v, signbit(v) AS s)
   ```
   
   `LogicalProject.computeFd` records `{v} -> {s}`, and the later default 
`EliminateGroupByKey` removes `s` (wrapping it in `any_value` when selected) 
without consulting `preservesGroupingEquality`. The remaining `v` key 
canonicalizes `+0/-0` together, so the expected two count-1 groups become one 
count-2 group. `atan2`, `pow`, and the string cast take the same path. This is 
distinct from the existing analyzer-rule thread, which explicitly assumed this 
rule was disabled. Please make the projected-expression FD/elimination 
grouping-equality-safe and keep a default-rule regression that proves the final 
plan retains these keys.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to