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 c45d1a60670 [fix](eager-agg) Handle duplicate aggregate functions
pushed through projects (#66531)
c45d1a60670 is described below
commit c45d1a6067067ee6066e92275a2013e858e917d1
Author: feiniaofeiafei <[email protected]>
AuthorDate: Wed Aug 12 11:04:19 2026 +0800
[fix](eager-agg) Handle duplicate aggregate functions pushed through
projects (#66531)
### What problem does this PR solve?
Related PR: #63690
Problem Summary:
Eager aggregation pushdown may fail when different aggregate functions
become
the same expression after passing through a Project. report error:
```text
2026-08-06 04:26:54,285 INFO (mysql-nio-pool-14|325)
[PushDownAggregation.visitLogicalAggregate():280] PushDownAggregation failed:
Cannot invoke
"org.apache.doris.nereids.trees.expressions.NamedExpression.toSlot()" because
"namedExpression" is null
at
org.apache.doris.nereids.rules.rewrite.eageraggregation.EagerAggRewriter.visitLogicalProject(EagerAggRewriter.java:718)
at
org.apache.doris.nereids.rules.rewrite.eageraggregation.EagerAggRewriter.visitLogicalProject(EagerAggRewriter.java:90)
at
org.apache.doris.nereids.trees.plans.logical.LogicalProject.accept(LogicalProject.java:160)
at
org.apache.doris.nereids.rules.rewrite.eageraggregation.EagerAggRewriter.visitLogicalUnion(EagerAggRewriter.java:582)
at
org.apache.doris.nereids.rules.rewrite.eageraggregation.EagerAggRewriter.visitLogicalUnion(EagerAggRewriter.java:90)
at
org.apache.doris.nereids.trees.plans.logical.LogicalUnion.accept(LogicalUnion.java:155)
```
For example:
```text
Aggregate: SUM(x)#4, SUM(y)#5
Union All
Project: 0 AS x, 0 AS y
Join
```
After pushing the aggregates through the Project, both functions
become`SUM(0)`:
```text
functions: [SUM(0), SUM(0)]
aliasMap: SUM(0) -> #5
```
Because `aliasMap` uses expression equality, only one entry is retained.
The Project still tries to read both `#4` and `#5` from
`BilateralState`,
causing a null lookup.
This PR deduplicates the child aggregate and records the ExprId mapping:
```text
child aggregate: SUM(0) -> #8
ExprId mapping: #4 -> #8, #5 -> #8
```
The Project then restores both required outputs:
```text
slot#8 AS slot#4
slot#8 AS slot#5
```
When no aggregate functions are merged, the original ExprIds are reused
to
avoid unnecessary aliases.
The same fix also covers cases such as:
```text
Project: a#1 AS x, a#1 AS y
```
where `SUM(x)` and `SUM(y)` both become `SUM(a#1)` after pushdown.
### Release note
None
### Check List (For Author)
- Test
- [x] Regression test
- `query_p0/eager_agg/bilateral_eager_agg`
- Covers two aggregate functions that become the same function after
Project pushdown.
- [ ] Unit Test
- [ ] Manual test
- [ ] No need to test or manual test.
- Behavior changed:
- [x] Yes.
- Prevents eager aggregation pushdown from failing when multiple
aggregate functions become identical after Project rewriting.
- `eager_aggregation_mode=1` can force eligible pushdown after a UNION.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../rewrite/eageraggregation/EagerAggRewriter.java | 78 +++++++++++++---------
.../query_p0/eager_agg/bilateral_eager_agg.out | 4 ++
.../data/query_p0/eager_agg/eager_agg.out | 8 ++-
.../query_p0/eager_agg/bilateral_eager_agg.groovy | 74 ++++++++++++++++++++
4 files changed, 131 insertions(+), 33 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/eageraggregation/EagerAggRewriter.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/eageraggregation/EagerAggRewriter.java
index a698459125c..e0219de031a 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/eageraggregation/EagerAggRewriter.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/eageraggregation/EagerAggRewriter.java
@@ -60,11 +60,13 @@ import org.apache.doris.qe.SessionVariable;
import org.apache.doris.statistics.ColumnStatistic;
import org.apache.doris.statistics.Statistics;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
-import java.util.IdentityHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -134,8 +136,8 @@ public class EagerAggRewriter extends
DefaultPlanRewriter<PushDownAggContext> {
// construct left and right aggFuncs and aliasMap
List<AggregateFunction> leftFuncs = new ArrayList<>();
List<AggregateFunction> rightFuncs = new ArrayList<>();
- Map<AggregateFunction, Alias> leftAliasMap = new IdentityHashMap<>();
- Map<AggregateFunction, Alias> rightAliasMap = new IdentityHashMap<>();
+ Map<AggregateFunction, Alias> leftAliasMap = new HashMap<>();
+ Map<AggregateFunction, Alias> rightAliasMap = new HashMap<>();
for (AggregateFunction f : context.getAggFunctions()) {
Set<Slot> inputs = f.getInputSlots();
Alias a = context.getAliasMap().get(f);
@@ -197,7 +199,7 @@ public class EagerAggRewriter extends
DefaultPlanRewriter<PushDownAggContext> {
}
private boolean isPassThroughHeavyJoin(Plan joinChild, PushDownAggContext
context) {
- if (context.isPassThroughHeavyJoin() ||
SessionVariable.getEagerAggregationMode() > 0) {
+ if (context.isPassThroughHeavyJoin()) {
return true;
} else {
Statistics stats = joinChild.getStats();
@@ -405,7 +407,7 @@ public class EagerAggRewriter extends
DefaultPlanRewriter<PushDownAggContext> {
private PushDownAggContext createContextFromProject(
LogicalProject<? extends Plan> project,
- PushDownAggContext context) {
+ PushDownAggContext context, Map<ExprId, ExprId>
projectToChildExprIdMap) {
/*
* context: sum(a) groupBy(y+z as x, l)
* proj: b+c as a, u+v as y, m+n as l
@@ -419,35 +421,40 @@ public class EagerAggRewriter extends
DefaultPlanRewriter<PushDownAggContext> {
.stream().map(slot -> (SlotReference)
slot).collect(Collectors.toList()));
}
- List<AggregateFunction> aggFunctions = new ArrayList<>();
- Map<AggregateFunction, Alias> aliasMap = new IdentityHashMap<>();
+ Set<AggregateFunction> aggFunctions = new LinkedHashSet<>();
+ Map<AggregateFunction, Alias> aliasMap = new HashMap<>();
+ boolean newContainsNullToNonNull = context.containsNullToNonNull;
+
for (AggregateFunction aggFunc : context.getAggFunctions()) {
AggregateFunction newAggFunc = (AggregateFunction)
project.pushDownExpressionPastProject(aggFunc);
Alias alias = context.getAliasMap().get(aggFunc);
- aliasMap.put(newAggFunc, (Alias) alias.withChildren(newAggFunc));
+ Alias aliasForChild;
+ if (aliasMap.containsKey(newAggFunc)) {
+ aliasForChild = aliasMap.get(newAggFunc);
+ } else {
+ aliasForChild = (Alias) alias.withChildren(newAggFunc);
+ aliasMap.put(newAggFunc, aliasForChild);
+ }
+ projectToChildExprIdMap.put(alias.getExprId(),
aliasForChild.getExprId());
aggFunctions.add(newAggFunc);
- }
- // After pushing expressions past the project, the agg functions may
now
- // contain NullToNonNull expressions that were hidden behind slot
references before.
- // e.g. count(#slot) where #slot = coalesce(a, 0) in the project.
- // We must re-check and update containsNullToNonNull accordingly.
- boolean newContainsNullToNonNull = context.containsNullToNonNull;
- if (!newContainsNullToNonNull) {
- for (AggregateFunction aggFunc : aggFunctions) {
- if (aggFunc.children().stream().anyMatch(
- arg -> arg.anyMatch(e ->
-
NullToNonNullFunction.canConvertNullToNonNull((Expression) e)))) {
- newContainsNullToNonNull = true;
- break;
- }
+
+ // After pushing expressions past the project, the agg functions
may now
+ // contain NullToNonNull expressions that were hidden behind slot
references before.
+ // e.g. count(#slot) where #slot = coalesce(a, 0) in the project.
+ // We must re-check and update containsNullToNonNull accordingly.
+ if (!newContainsNullToNonNull
+ && newAggFunc.children().stream().anyMatch(
+ arg -> arg.anyMatch(e ->
+
NullToNonNullFunction.canConvertNullToNonNull((Expression) e)))) {
+ newContainsNullToNonNull = true;
}
}
- PushDownAggContext newContext = new PushDownAggContext(aggFunctions,
groupKeys, aliasMap,
+
+ return new PushDownAggContext(ImmutableList.copyOf(aggFunctions),
groupKeys, aliasMap,
context.getCascadesContext(), context.isPassThroughHeavyJoin(),
context.hasDecomposedAggIf, newContainsNullToNonNull,
context.getBilateralState(), context.needOutputCount(),
context.isPassThroughJoinOrUnion(),
context.isSmallBroadcastBottomJoin());
- return newContext;
}
private boolean canPushThroughProject(LogicalProject<? extends Plan>
project, PushDownAggContext context) {
@@ -557,7 +564,7 @@ public class EagerAggRewriter extends
DefaultPlanRewriter<PushDownAggContext> {
Plan child = union.children().get(idx);
final int childIdx = idx;
List<AggregateFunction> aggFunctionsForChild = new ArrayList<>();
- IdentityHashMap<AggregateFunction, Alias> aliasMapForChild = new
IdentityHashMap<>();
+ Map<AggregateFunction, Alias> aliasMapForChild = new HashMap<>();
for (AggregateFunction func : context.getAggFunctions()) {
AggregateFunction newFunc = (AggregateFunction)
union.pushDownExpressionPastSetOperator(func, childIdx);
aggFunctionsForChild.add(newFunc);
@@ -686,7 +693,8 @@ public class EagerAggRewriter extends
DefaultPlanRewriter<PushDownAggContext> {
if (!canPushThroughProject(project, context)) {
return genAggregate(project, context);
}
- PushDownAggContext newContext = createContextFromProject(project,
context);
+ Map<ExprId, ExprId> projectToChildExprIdMap = new HashMap<>();
+ PushDownAggContext newContext = createContextFromProject(project,
context, projectToChildExprIdMap);
if (newContext.aggFuncAndGroupKeyAllEmpty()) {
return project;
}
@@ -714,9 +722,19 @@ public class EagerAggRewriter extends
DefaultPlanRewriter<PushDownAggContext> {
BilateralState state = context.getBilateralState();
for (AggregateFunction aggFunc : context.getAggFunctions()) {
Alias alias = context.getAliasMap().get(aggFunc);
- NamedExpression namedExpression =
state.getPushedAggFuncSlot(alias.getExprId());
- newProjections.add(namedExpression.toSlot());
+ ExprId childExprId =
projectToChildExprIdMap.get(alias.getExprId());
+ NamedExpression namedExpression =
state.getPushedAggFuncSlot(childExprId);
+ NamedExpression output;
+ if (namedExpression.getExprId().equals(alias.getExprId())) {
+ output = namedExpression.toSlot();
+ } else {
+ output = (Alias)
alias.withChildren(namedExpression.toSlot());
+ state.registerAggFuncOutput(alias.getExprId(),
output.toSlot(),
+ state.isAggFuncActuallyPushed(childExprId));
+ }
+ newProjections.add(output);
}
+
for (SlotReference slot : context.getGroupKeys()) {
boolean valid = false;
for (NamedExpression ne : project.getProjects()) {
@@ -1304,9 +1322,7 @@ public class EagerAggRewriter extends
DefaultPlanRewriter<PushDownAggContext> {
}
if (mode > 0) {
- // when mode=1, any join is regarded as big join in order to
- // push down aggregation through at least one join
- return context.isPassThroughHeavyJoin();
+ return true;
}
if (!context.isPassThroughHeavyJoin() && !context.hasDecomposedAggIf) {
diff --git a/regression-test/data/query_p0/eager_agg/bilateral_eager_agg.out
b/regression-test/data/query_p0/eager_agg/bilateral_eager_agg.out
index a386ad329a1..1ff0e1a9dc8 100644
--- a/regression-test/data/query_p0/eager_agg/bilateral_eager_agg.out
+++ b/regression-test/data/query_p0/eager_agg/bilateral_eager_agg.out
@@ -335,3 +335,7 @@
2000-06-03 true
2020-01-01 false
+-- !union_2_same_agg_func --
+1 10 10
+2 20 20
+
diff --git a/regression-test/data/query_p0/eager_agg/eager_agg.out
b/regression-test/data/query_p0/eager_agg/eager_agg.out
index c0ed2babc99..b044573b029 100644
--- a/regression-test/data/query_p0/eager_agg/eager_agg.out
+++ b/regression-test/data/query_p0/eager_agg/eager_agg.out
@@ -307,8 +307,12 @@ PhysicalResultSink
------PhysicalUnion
--------hashJoin[INNER_JOIN] hashCondition=((dt.d_date_sk =
ss.ss_sold_date_sk)) otherCondition=()
----------PhysicalOlapScan[store_sales(ss)]
-----------PhysicalOlapScan[date_dim(dt)]
---------PhysicalOlapScan[date_dim]
+----------hashAgg[GLOBAL]
+------------hashAgg[LOCAL]
+--------------PhysicalOlapScan[date_dim(dt)]
+--------hashAgg[GLOBAL]
+----------hashAgg[LOCAL]
+------------PhysicalOlapScan[date_dim]
Hint log:
Used:
diff --git
a/regression-test/suites/query_p0/eager_agg/bilateral_eager_agg.groovy
b/regression-test/suites/query_p0/eager_agg/bilateral_eager_agg.groovy
index 76c1cc5f1d7..d59fc4e1896 100644
--- a/regression-test/suites/query_p0/eager_agg/bilateral_eager_agg.groovy
+++ b/regression-test/suites/query_p0/eager_agg/bilateral_eager_agg.groovy
@@ -950,4 +950,78 @@ suite("bilateral_eager_agg") {
WHERE l.filter_date = '2018-01-08'
GROUP BY group_flag;
"""
+
+ multi_sql """
+ DROP TABLE IF EXISTS src_a;
+ DROP TABLE IF EXISTS src_b;
+ DROP TABLE IF EXISTS src_c;
+
+ CREATE TABLE src_a (
+ k BIGINT NOT NULL,
+ v BIGINT NOT NULL
+ )
+ DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1"
+ );
+
+ CREATE TABLE src_b (
+ k BIGINT NOT NULL,
+ join_id BIGINT NOT NULL
+ )
+ DUPLICATE KEY(k, join_id)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1"
+ );
+
+ CREATE TABLE src_c (
+ join_id BIGINT NOT NULL
+ )
+ DUPLICATE KEY(join_id)
+ DISTRIBUTED BY HASH(join_id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1"
+ );
+
+ INSERT INTO src_a VALUES
+ (1, 10),
+ (2, 20);
+
+ INSERT INTO src_b VALUES
+ (1, 101),
+ (2, 102);
+
+ INSERT INTO src_c VALUES
+ (101),
+ (102);
+
+ SET disable_join_reorder = true;
+ SET eager_aggregation_mode = 1;
+ SET fe_debug = true;
+ """
+
+ order_qt_union_2_same_agg_func """
+ SELECT
+ u.k,
+ SUM(u.x) AS sum_x,
+ SUM(u.y) AS sum_y
+ FROM (
+ SELECT
+ a.k,
+ a.v AS x,
+ a.v AS y
+ FROM src_a a
+ UNION ALL
+ SELECT
+ b.k,
+ CAST(0 AS BIGINT) AS x,
+ CAST(0 AS BIGINT) AS y
+ FROM src_b b
+ INNER JOIN src_c c
+ ON b.join_id = c.join_id
+ ) u
+ GROUP BY u.k;
+ """
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]