Skip to content

Commit 55d0b23

Browse files
committed
Improve aggregation wrapper rewriting and merge evaluation
1 parent a9ef85f commit 55d0b23

16 files changed

Lines changed: 949 additions & 82 deletions

File tree

RELEASE-NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
1. Sharding: Fix HASH_MOD routing mismatch for same negative numeric values across numeric Java types with compatibility switch `normalize-numeric-int-range` - [#38327](https://github.com/apache/shardingsphere/pull/38327)
7373
1. Proxy Native: Support building Proxy Native via GraalVM CE for JDK 25 - [#38682](https://github.com/apache/shardingsphere/pull/38682)
7474
1. SQL Parser: Support SQLServer table variable declaration parse - [#38904](https://github.com/apache/shardingsphere/pull/38904)
75-
1. Support evaluation of IFNULL/COALESCE expressions over merged aggregation results - [#38990](https://github.com/apache/shardingsphere/pull/38990)
75+
1. Support evaluation of IFNULL/COALESCE scalar wrappers (with literal or nested-aggregation fallbacks) over merged aggregation results - [#38990](https://github.com/apache/shardingsphere/pull/38990)
7676

7777
## Release 5.5.3
7878

docs/document/content/reference/sharding/merge.en.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ Stream group-by merger is different from order-by merger only in two aspects:
6767
1. It will take out all the data with the same group item from multiple data result sets at once.
6868
1. It carried out the aggregation calculation according to the aggregation function type.
6969

70+
Expression-derived aggregations
71+
72+
In many real-world queries, aggregation functions are wrapped inside scalar expressions or conditional functions (for example, `IFNULL(SUM(score), 0)` or `COALESCE(COUNT(id), 0)`). The merger engine now natively evaluates such expression-derived aggregations during both stream and memory merges. Expression nodes that wrap aggregation calls are recognized and the underlying aggregation is computed as part of the usual merge flow; surrounding scalar or conditional expressions are then applied to the aggregated result so that semantics (null coalescing, defaulting, arithmetic, etc.) are preserved.
73+
74+
Proper empty / no-route initializations
75+
76+
When a group has no matching rows (including cases where a shard has no route for the query), expression-derived aggregations that depend on counts are initialized safely. In particular, `COUNT` cells are initialized to `0` (rather than relying on generic defaults) so that subsequent scalar expressions (e.g., `IFNULL`, `COALESCE`, or arithmetic) produce correct and deterministic results for empty or unrouted groups.
77+
7078
For the inconsistency between the grouping item and ordering item, it requires uploading all the data to the memory to group and aggregate, since the relevant data value needed to acquire group information is not continuous, and stream merger is not available. For example, acquire each examinee’s total score through the following SQL and order them from the highest to the lowest:
7179

7280
```sql
@@ -87,6 +95,16 @@ The sum aggregation function refers to `SUM` and `COUNT`. They need to sum up al
8795

8896
The average aggregation function refers only to `AVG`. It must be calculated through `SUM` and `COUNT` rewritten by SQL, which has been mentioned in the SQL rewriting section.
8997

98+
## Performance / Memory Optimization
99+
100+
Zero-copy projection pass-through
101+
102+
The group-by merger avoids creating redundant collection wrappers (for example, copying expanded projection results into new `ArrayList` instances) when evaluating projections during merges. Instead, the merger streams directly against existing projection context references and iterates over the original projection objects where possible. This zero-copy pass-through reduces temporary allocations and keeps the garbage collection profile low during large merges.
103+
104+
Elimination of quadratic overhead
105+
106+
Previous implementations performed repeated linked-list scans when resolving projections across expanded projection sets, which could lead to quadratic (`O(N^2)`) behavior for large projection counts. Those linked-list scans have been replaced by optimized linear lookup mappings (e.g., direct index maps or hash-based lookups) so projection evaluation now completes in linear time relative to the number of projections.
107+
90108
## Pagination Merger
91109

92110
All the merger types above can be paginated. Pagination is the decorator added to other kinds of mergers. ShardingSphere strengthens its ability to paginate the data result set through decorator mode. The pagination merger is responsible for filtering unnecessary data.

features/sharding/core/src/main/java/org/apache/shardingsphere/sharding/merge/dql/groupby/AggregationWrapperExpressionEvaluator.java

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.expr.simple.LiteralExpressionSegment;
2525
import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.item.AggregationProjectionSegment;
2626

27+
import java.math.BigDecimal;
2728
import java.util.List;
2829
import java.util.function.Function;
2930

@@ -37,41 +38,73 @@ public final class AggregationWrapperExpressionEvaluator {
3738
* @param expression expression segment to evaluate
3839
* @param derivedAggregations derived aggregations
3940
* @param row memory query result row
41+
* @param targetType the target JDBC class type to coerce to (optional)
4042
* @return evaluated result
4143
*/
42-
public static Object evaluate(final ExpressionSegment expression, final List<AggregationProjection> derivedAggregations, final MemoryQueryResultRow row) {
43-
return evaluate(expression, derivedAggregations, row::getCell);
44+
public static Object evaluate(final ExpressionSegment expression, final List<AggregationProjection> derivedAggregations, final MemoryQueryResultRow row, final Class<?> targetType) {
45+
Object result = evaluate(expression, derivedAggregations, row::getCell);
46+
return coerce(result, targetType);
4447
}
4548

4649
/**
4750
* Evaluate expression with current row data.
4851
* @param expression expression segment to evaluate
4952
* @param derivedAggregations derived aggregations
5053
* @param currentRow current row data
54+
* @param targetType the target JDBC class type to coerce to (optional)
5155
* @return evaluated result
5256
*/
53-
public static Object evaluate(final ExpressionSegment expression, final List<AggregationProjection> derivedAggregations, final List<Object> currentRow) {
54-
return evaluate(expression, derivedAggregations, index -> currentRow.get(index - 1));
57+
public static Object evaluate(final ExpressionSegment expression, final List<AggregationProjection> derivedAggregations, final List<Object> currentRow, final Class<?> targetType) {
58+
Object result = evaluate(expression, derivedAggregations, index -> currentRow.get(index - 1));
59+
return coerce(result, targetType);
5560
}
5661

57-
private static Object evaluate(final ExpressionSegment expression, final List<AggregationProjection> derivedAggregations, final Function<Integer, Object> valueProvider) {
62+
private static Object evaluate(final ExpressionSegment expression, final List<AggregationProjection> derivedAggregations, final Function<Integer, Object> valueExtractor) {
5863
if (expression instanceof AggregationProjectionSegment) {
59-
return getMergedAggregationValue((AggregationProjectionSegment) expression, derivedAggregations, valueProvider);
64+
return getMergedAggregationValue((AggregationProjectionSegment) expression, derivedAggregations, valueExtractor);
6065
}
6166
if (expression instanceof LiteralExpressionSegment) {
6267
return ((LiteralExpressionSegment) expression).getLiterals();
6368
}
6469
if (expression instanceof FunctionSegment) {
65-
return evaluateFunction((FunctionSegment) expression, derivedAggregations, valueProvider);
70+
return evaluateFunction((FunctionSegment) expression, derivedAggregations, valueExtractor);
6671
}
6772
throw new IllegalArgumentException(String.format("Unsupported aggregation wrapper expression segment type: %s", expression.getClass().getName()));
6873
}
6974

70-
private static Object evaluateFunction(final FunctionSegment functionSegment, final List<AggregationProjection> derivedAggregations, final Function<Integer, Object> valueProvider) {
75+
private static Object coerce(final Object value, final Class<?> targetType) {
76+
if (value == null || targetType == null || value.getClass().equals(targetType)) {
77+
return value;
78+
}
79+
if (value instanceof Number) {
80+
Number num = (Number) value;
81+
if (targetType == BigDecimal.class) {
82+
return new BigDecimal(num.toString());
83+
}
84+
if (targetType == Long.class) {
85+
return num.longValue();
86+
}
87+
if (targetType == Integer.class) {
88+
return num.intValue();
89+
}
90+
if (targetType == Double.class) {
91+
return num.doubleValue();
92+
}
93+
if (targetType == Float.class) {
94+
return num.floatValue();
95+
}
96+
if (targetType == Short.class) {
97+
return num.shortValue();
98+
}
99+
}
100+
return value;
101+
}
102+
103+
private static Object evaluateFunction(final FunctionSegment functionSegment, final List<AggregationProjection> derivedAggregations, final Function<Integer, Object> valueExtractor) {
71104
String functionName = functionSegment.getFunctionName();
72105
if ("IFNULL".equalsIgnoreCase(functionName) || "COALESCE".equalsIgnoreCase(functionName)) {
73106
for (ExpressionSegment each : functionSegment.getParameters()) {
74-
Object value = evaluate(each, derivedAggregations, valueProvider);
107+
Object value = evaluate(each, derivedAggregations, valueExtractor);
75108
if (null != value) {
76109
return value;
77110
}
@@ -81,10 +114,10 @@ private static Object evaluateFunction(final FunctionSegment functionSegment, fi
81114
throw new IllegalArgumentException(String.format("Unsupported aggregation wrapper function: %s", functionName));
82115
}
83116

84-
private static Object getMergedAggregationValue(final AggregationProjectionSegment segment, final List<AggregationProjection> derivedAggregations, final Function<Integer, Object> valueProvider) {
117+
private static Object getMergedAggregationValue(final AggregationProjectionSegment segment, final List<AggregationProjection> derivedAggregations, final Function<Integer, Object> valueExtractor) {
85118
for (AggregationProjection each : derivedAggregations) {
86119
if (each.getExpression().equals(segment.getText())) {
87-
return valueProvider.apply(each.getIndex());
120+
return valueExtractor.apply(each.getIndex());
88121
}
89122
}
90123
throw new IllegalArgumentException(String.format("Cannot find merged aggregation value for expression: %s", segment.getText()));

features/sharding/core/src/main/java/org/apache/shardingsphere/sharding/merge/dql/groupby/GroupByMemoryMergedResult.java

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,9 @@
4242
import java.util.ArrayList;
4343
import java.util.Collections;
4444
import java.util.HashMap;
45-
import java.util.LinkedList;
4645
import java.util.List;
4746
import java.util.Map;
4847
import java.util.Map.Entry;
49-
import java.util.function.Function;
50-
import java.util.stream.Collectors;
5148

5249
/**
5350
* Memory merged result for group by.
@@ -83,9 +80,13 @@ private void initForFirstGroupByValue(final SelectStatementContext selectStateme
8380
if (!dataMap.containsKey(groupByValue)) {
8481
dataMap.put(groupByValue, new MemoryQueryResultRow(queryResult));
8582
}
86-
aggregationMap.computeIfAbsent(groupByValue, unused -> selectStatementContext.getProjectionsContext().getAggregationProjections().stream()
87-
.collect(Collectors.toMap(Function.identity(),
88-
input -> AggregationUnitFactory.create(input.getType(), input instanceof AggregationDistinctProjection, input.getSeparator().orElse(null)))));
83+
if (!aggregationMap.containsKey(groupByValue)) {
84+
Map<AggregationProjection, AggregationUnit> units = new HashMap<>(selectStatementContext.getProjectionsContext().getAggregationProjections().size(), 1F);
85+
for (AggregationProjection each : selectStatementContext.getProjectionsContext().getAggregationProjections()) {
86+
units.put(each, AggregationUnitFactory.create(each.getType(), each instanceof AggregationDistinctProjection, each.getSeparator().orElse(null)));
87+
}
88+
aggregationMap.put(groupByValue, units);
89+
}
8990
}
9091

9192
private void aggregate(final SelectStatementContext selectStatementContext, final QueryResult queryResult,
@@ -152,7 +153,13 @@ private List<MemoryQueryResultRow> getMemoryResultSetRows(final SelectStatementC
152153
}
153154
Object[] data = generateReturnData(selectStatementContext);
154155
MemoryQueryResultRow syntheticRow = new MemoryQueryResultRow(data);
155-
evaluateExpressionValue(selectStatementContext, syntheticRow);
156+
157+
Map<ExpressionProjection, List<AggregationProjection>> expressionDerivedAggregations = selectStatementContext.getProjectionsContext().getExpressionDerivedAggregations();
158+
if (expressionDerivedAggregations != null && !expressionDerivedAggregations.isEmpty()) {
159+
Map<ExpressionProjection, Integer> expressionIndices = getExpressionIndices(selectStatementContext, expressionDerivedAggregations);
160+
evaluateExpressionValue(expressionDerivedAggregations, expressionIndices, syntheticRow);
161+
}
162+
156163
return Collections.singletonList(syntheticRow);
157164
}
158165
List<MemoryQueryResultRow> result = new ArrayList<>(dataMap.values());
@@ -161,45 +168,88 @@ private List<MemoryQueryResultRow> getMemoryResultSetRows(final SelectStatementC
161168
}
162169

163170
private Object[] generateReturnData(final SelectStatementContext selectStatementContext) {
164-
List<Projection> projections = new LinkedList<>(selectStatementContext.getProjectionsContext().getExpandProjections());
171+
int maxColumnIndex = calculateMaxColumnIndex(selectStatementContext);
172+
Object[] result = new Object[maxColumnIndex];
173+
174+
List<Projection> expandProjections = selectStatementContext.getProjectionsContext().getExpandProjections();
175+
for (int i = 0; i < expandProjections.size(); i++) {
176+
if (expandProjections.get(i) instanceof AggregationProjection && AggregationType.COUNT == ((AggregationProjection) expandProjections.get(i)).getType()) {
177+
result[i] = 0;
178+
}
179+
}
165180

166-
int maxColumnIndex = projections.size();
167181
for (AggregationProjection each : selectStatementContext.getProjectionsContext().getAggregationProjections()) {
168-
maxColumnIndex = Math.max(maxColumnIndex, each.getIndex());
182+
if (AggregationType.COUNT == each.getType() && each.getIndex() > 0) {
183+
result[each.getIndex() - 1] = 0;
184+
}
169185
}
170186

171-
Object[] result = new Object[maxColumnIndex];
172-
for (int i = 0; i < projections.size(); i++) {
173-
if (projections.get(i) instanceof AggregationProjection && AggregationType.COUNT == ((AggregationProjection) projections.get(i)).getType()) {
174-
result[i] = 0;
187+
for (List<AggregationProjection> derivedList : selectStatementContext.getProjectionsContext().getExpressionDerivedAggregations().values()) {
188+
for (AggregationProjection each : derivedList) {
189+
if (AggregationType.COUNT == each.getType() && each.getIndex() > 0) {
190+
result[each.getIndex() - 1] = 0;
191+
}
175192
}
176193
}
177194
return result;
178195
}
179196

180-
private void setExpressionValueToMemoryRow(final SelectStatementContext selectStatementContext, final Map<GroupByValue, MemoryQueryResultRow> dataMap) {
181-
for (MemoryQueryResultRow each : dataMap.values()) {
182-
evaluateExpressionValue(selectStatementContext, each);
197+
private int calculateMaxColumnIndex(final SelectStatementContext selectStatementContext) {
198+
int maxColumnIndex = selectStatementContext.getProjectionsContext().getExpandProjections().size();
199+
200+
for (AggregationProjection each : selectStatementContext.getProjectionsContext().getAggregationProjections()) {
201+
maxColumnIndex = Math.max(maxColumnIndex, each.getIndex());
183202
}
203+
204+
for (List<AggregationProjection> derivedList : selectStatementContext.getProjectionsContext().getExpressionDerivedAggregations().values()) {
205+
for (AggregationProjection each : derivedList) {
206+
maxColumnIndex = Math.max(maxColumnIndex, each.getIndex());
207+
}
208+
}
209+
return maxColumnIndex;
184210
}
185211

186-
private void evaluateExpressionValue(final SelectStatementContext selectStatementContext, final MemoryQueryResultRow row) {
212+
private void setExpressionValueToMemoryRow(final SelectStatementContext selectStatementContext, final Map<GroupByValue, MemoryQueryResultRow> dataMap) {
187213
Map<ExpressionProjection, List<AggregationProjection>> expressionDerivedAggregations = selectStatementContext.getProjectionsContext().getExpressionDerivedAggregations();
188214
if (expressionDerivedAggregations == null || expressionDerivedAggregations.isEmpty()) {
189215
return;
190216
}
191217

192-
List<Projection> expandProjections = new ArrayList<>(selectStatementContext.getProjectionsContext().getExpandProjections());
193-
218+
Map<ExpressionProjection, Integer> expressionIndices = getExpressionIndices(selectStatementContext, expressionDerivedAggregations);
219+
for (MemoryQueryResultRow each : dataMap.values()) {
220+
evaluateExpressionValue(expressionDerivedAggregations, expressionIndices, each);
221+
}
222+
}
223+
224+
private Map<ExpressionProjection, Integer> getExpressionIndices(final SelectStatementContext selectStatementContext,
225+
final Map<ExpressionProjection, List<AggregationProjection>> expressionDerivedAggregations) {
226+
List<Projection> expandProjections = selectStatementContext.getProjectionsContext().getExpandProjections();
227+
Map<ExpressionProjection, Integer> expressionIndices = new HashMap<>(expressionDerivedAggregations.size(), 1F);
228+
for (ExpressionProjection each : expressionDerivedAggregations.keySet()) {
229+
expressionIndices.put(each, expandProjections.indexOf(each) + 1);
230+
}
231+
return expressionIndices;
232+
}
233+
234+
private void evaluateExpressionValue(final Map<ExpressionProjection, List<AggregationProjection>> expressionDerivedAggregations,
235+
final Map<ExpressionProjection, Integer> expressionIndices, final MemoryQueryResultRow row) {
194236
for (Entry<ExpressionProjection, List<AggregationProjection>> exprEntry : expressionDerivedAggregations.entrySet()) {
237+
238+
int columnIndex = expressionIndices.getOrDefault(exprEntry.getKey(), -1);
239+
240+
Class<?> targetType = null;
241+
if (columnIndex > 0) {
242+
Object existingValue = row.getCell(columnIndex);
243+
targetType = existingValue != null ? existingValue.getClass() : null;
244+
}
245+
195246
Object evaluatedValue = AggregationWrapperExpressionEvaluator.evaluate(
196247
exprEntry.getKey().getExpressionSegment().getExpr(),
197248
exprEntry.getValue(),
198-
row);
249+
row,
250+
targetType);
199251

200-
int columnIndex = expandProjections.indexOf(exprEntry.getKey()) + 1;
201-
202-
if (columnIndex > 0) {
252+
if (columnIndex > 0 && evaluatedValue != null) {
203253
row.setCell(columnIndex, evaluatedValue);
204254
}
205255
}

0 commit comments

Comments
 (0)