This is an automated email from the ASF dual-hosted git repository.
ulysses-you pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 1db5e6863f7f [SPARK-32750][SQL] Support whole-stage codegen for
SortAggregate with grouping keys
1db5e6863f7f is described below
commit 1db5e6863f7f99a228f5b97664bb4a5c81def567
Author: Xiduo You <[email protected]>
AuthorDate: Wed Jul 15 14:52:21 2026 +0800
[SPARK-32750][SQL] Support whole-stage codegen for SortAggregate with
grouping keys
### What changes were proposed in this pull request?
This PR adds whole-stage code-gen support for `SortAggregateExec` when it
has grouping
keys. Previously only the no-grouping-keys path was code-gen'd; with keys
it fell back to
the interpreted `SortBasedAggregationIterator`.
The implementation:
- `AggregateCodegenSupport` is refactored to share the aggregation-buffer
creation
(`createAggBufVars`) and buffer-update (`generateAggBufferUpdateCode`)
logic between the
no-keys path and the new sort-based with-keys path.
- `SortAggregateExec` implements `doProduceWithKeys` / `doConsumeWithKeys`.
Since the input
is sorted by the grouping keys, a group's result is emitted as soon as
the next group
starts (detected by comparing the binary representation of the grouping
key). The produce
loop is resumable across `processNext` invocations via `shouldStop()`, so
a completed
group's output row is not overwritten by the reused output buffer.
- The with-keys path only supports binary-stable grouping-key types
(guarded in
`supportCodegen`), because group boundaries are detected via
`UnsafeRow.equals`.
- A new internal config
`spark.sql.codegen.aggregate.sortAggregate.withKeys.enabled`
(default true) gates this path; it takes effect only when
`spark.sql.codegen.aggregate.sortAggregate.enabled` is enabled.
### Why are the changes needed?
Sort aggregate with grouping keys was the only remaining aggregate path
without whole-stage
code-gen, forcing it through the slower interpreted iterator.
`SortAggregateBenchmark` shows
a consistent 1.2~1.3x speedup for grouped aggregates on the code-gen path.
### Does this PR introduce _any_ user-facing change?
No. This is an internal code-gen optimization; results are unchanged. The
new config is
`internal()`.
### How was this patch tested?
- New tests in `WholeStageCodegenSuite` covering: multiple/numeric/string
grouping keys,
float/double keys (including `-0.0` and `NaN`), decimal keys, null keys
and values,
single-row groups, a single all-rows group, grouping-only aggregates (no
aggregate
function, e.g. `DISTINCT`), downstream limit (resumable `shouldStop`
path), empty input,
single partition, split aggregate functions, FILTER clauses, HAVING-style
filters, and the
config gate. Each test asserts a code-gen'd `SortAggregateExec` in the
plan and checks the
result matches the interpreted (code-gen disabled) result.
- Added `SortAggregateBenchmark` with results committed for JDK 17/21/25.
The generated code of a simple query: `select id , count(*) from t1 group
by id`:
```
/* 001 */ public Object generate(Object[] references) {
/* 002 */ return new GeneratedIteratorForCodegenStage1(references);
/* 003 */ }
/* 004 */
/* 005 */ // codegenStageId=1
/* 006 */ final class GeneratedIteratorForCodegenStage1 extends
org.apache.spark.sql.execution.BufferedRowIterator {
/* 007 */ private Object[] references;
/* 008 */ private scala.collection.Iterator[] inputs;
/* 009 */ private boolean sortAgg_bufIsNull_0;
/* 010 */ private long sortAgg_bufValue_0;
/* 011 */ private UnsafeRow sortAgg_currentGroupingKey_0;
/* 012 */ private boolean sortAgg_initGroup_0;
/* 013 */ private boolean sortAgg_noMoreInputTerm_0;
/* 014 */ private boolean sort_needToSort_0;
/* 015 */ private org.apache.spark.sql.execution.UnsafeExternalRowSorter
sort_sorter_0;
/* 016 */ private org.apache.spark.executor.TaskMetrics sort_metrics_0;
/* 017 */ private scala.collection.Iterator<UnsafeRow> sort_sortedIter_0;
/* 018 */ private int columnartorow_batchIdx_0;
/* 019 */ private org.apache.spark.sql.vectorized.ColumnarBatch[]
columnartorow_mutableStateArray_1 = new
org.apache.spark.sql.vectorized.ColumnarBatch[1];
/* 020 */ private
org.apache.spark.sql.execution.vectorized.OnHeapColumnVector[]
columnartorow_mutableStateArray_2 = new
org.apache.spark.sql.execution.vectorized.OnHeapColumnVector[1];
/* 021 */ private scala.collection.Iterator[]
columnartorow_mutableStateArray_0 = new scala.collection.Iterator[1];
/* 022 */ private
org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter[]
sortAgg_mutableStateArray_0 = new
org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter[3];
/* 023 */
/* 024 */ public GeneratedIteratorForCodegenStage1(Object[] references) {
/* 025 */ this.references = references;
/* 026 */ }
/* 027 */
/* 028 */ public void init(int index, scala.collection.Iterator[] inputs)
{
/* 029 */ partitionIndex = index;
/* 030 */ this.inputs = inputs;
/* 031 */
/* 032 */ sortAgg_mutableStateArray_0[0] = new
org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(2, 0);
/* 033 */ sort_needToSort_0 = true;
/* 034 */ sort_sorter_0 = ((org.apache.spark.sql.execution.SortExec)
references[1] /* plan */).createSorter();
/* 035 */ sort_metrics_0 =
org.apache.spark.TaskContext.get().taskMetrics();
/* 036 */ columnartorow_mutableStateArray_0[0] = inputs[0];
/* 037 */ sortAgg_mutableStateArray_0[1] = new
org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(1, 0);
/* 038 */ sortAgg_mutableStateArray_0[2] = new
org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(1, 0);
/* 039 */
/* 040 */ }
/* 041 */
/* 042 */ private void sortAgg_doConsume_0(InternalRow sort_outputRow_0,
long sortAgg_expr_0_0, boolean sortAgg_exprIsNull_0_0) throws
java.io.IOException {
/* 043 */ sortAgg_mutableStateArray_0[2].reset();
/* 044 */
/* 045 */ sortAgg_mutableStateArray_0[2].zeroOutNullBytes();
/* 046 */
/* 047 */ sortAgg_mutableStateArray_0[2].writeNullable(0,
sortAgg_expr_0_0, sortAgg_exprIsNull_0_0);
/* 048 */ if (!sortAgg_initGroup_0) {
/* 049 */ sortAgg_initGroup_0 = true;
/* 050 */ sortAgg_currentGroupingKey_0 =
(sortAgg_mutableStateArray_0[2].getRow()).copy();
/* 051 */ sortAgg_bufIsNull_0 = false;
/* 052 */ sortAgg_bufValue_0 = 0L;
/* 053 */ } else if
(!sortAgg_currentGroupingKey_0.equals((sortAgg_mutableStateArray_0[2].getRow())))
{
/* 054 */ sortAgg_doAggregateWithKeysOutput_0();
/* 055 */ sortAgg_currentGroupingKey_0 =
(sortAgg_mutableStateArray_0[2].getRow()).copy();
/* 056 */ sortAgg_bufIsNull_0 = false;
/* 057 */ sortAgg_bufValue_0 = 0L;
/* 058 */ }
/* 059 */
/* 060 */ // do aggregate
/* 061 */ // common sub-expressions
/* 062 */
/* 063 */ // evaluate aggregate functions and update aggregation buffers
/* 064 */
/* 065 */ long sortAgg_value_7 = -1L;
/* 066 */
/* 067 */ sortAgg_value_7 =
org.apache.spark.sql.catalyst.util.MathUtils.addExact(sortAgg_bufValue_0, 1L,
((org.apache.spark.sql.catalyst.trees.SQLQueryContext) references[7] /* errCtx
*/));
/* 068 */
/* 069 */ sortAgg_bufIsNull_0 = false;
/* 070 */ sortAgg_bufValue_0 = sortAgg_value_7;
/* 071 */
/* 072 */ }
/* 073 */
/* 074 */ private void wholestagecodegen_doAggregateWithKeys_0(int
partitionIndex) throws java.io.IOException {
/* 075 */ if (sort_needToSort_0) {
/* 076 */ long sort_spillSizeBefore_0 =
sort_metrics_0.memoryBytesSpilled();
/* 077 */ sort_addToSorter_0(partitionIndex);
/* 078 */ sort_sortedIter_0 = sort_sorter_0.sort();
/* 079 */ ((org.apache.spark.sql.execution.metric.SQLMetric)
references[6] /* sortTime */).add(sort_sorter_0.getSortTimeNanos() / 1000000);
/* 080 */ ((org.apache.spark.sql.execution.metric.SQLMetric)
references[4] /* peakMemory */).add(sort_sorter_0.getPeakMemoryUsage());
/* 081 */ ((org.apache.spark.sql.execution.metric.SQLMetric)
references[5] /* spillSize */).add(sort_metrics_0.memoryBytesSpilled() -
sort_spillSizeBefore_0);
/* 082 */
sort_metrics_0.incPeakExecutionMemory(sort_sorter_0.getPeakMemoryUsage());
/* 083 */ sort_needToSort_0 = false;
/* 084 */ }
/* 085 */
/* 086 */ while ( sort_sortedIter_0.hasNext()) {
/* 087 */ UnsafeRow sort_outputRow_0 =
(UnsafeRow)sort_sortedIter_0.next();
/* 088 */
/* 089 */ boolean sort_isNull_0 = sort_outputRow_0.isNullAt(0);
/* 090 */ long sort_value_0 = sort_isNull_0 ?
/* 091 */ -1L : (sort_outputRow_0.getLong(0));
/* 092 */
/* 093 */ sortAgg_doConsume_0(sort_outputRow_0, sort_value_0,
sort_isNull_0);
/* 094 */
/* 095 */ if (shouldStop()) return;
/* 096 */ }
/* 097 */
/* 098 */ }
/* 099 */
/* 100 */ private void sort_addToSorter_0(int partitionIndex) throws
java.io.IOException {
/* 101 */ if (columnartorow_mutableStateArray_1[0] == null) {
/* 102 */ columnartorow_nextBatch_0();
/* 103 */ }
/* 104 */ while ( columnartorow_mutableStateArray_1[0] != null) {
/* 105 */ int columnartorow_numRows_0 =
columnartorow_mutableStateArray_1[0].numRows();
/* 106 */ int columnartorow_localEnd_0 = columnartorow_numRows_0 -
columnartorow_batchIdx_0;
/* 107 */ for (int columnartorow_localIdx_0 = 0;
columnartorow_localIdx_0 < columnartorow_localEnd_0;
columnartorow_localIdx_0++) {
/* 108 */ int columnartorow_rowIdx_0 = columnartorow_batchIdx_0 +
columnartorow_localIdx_0;
/* 109 */ boolean columnartorow_isNull_0 =
columnartorow_mutableStateArray_2[0].isNullAt(columnartorow_rowIdx_0);
/* 110 */ long columnartorow_value_0 = columnartorow_isNull_0 ? -1L
: (columnartorow_mutableStateArray_2[0].getLong(columnartorow_rowIdx_0));
/* 111 */ sortAgg_mutableStateArray_0[1].reset();
/* 112 */
/* 113 */ sortAgg_mutableStateArray_0[1].zeroOutNullBytes();
/* 114 */
/* 115 */ sortAgg_mutableStateArray_0[1].writeNullable(0,
columnartorow_value_0, columnartorow_isNull_0);
/* 116 */
sort_sorter_0.insertRow((UnsafeRow)(sortAgg_mutableStateArray_0[1].getRow()));
/* 117 */ // shouldStop check is eliminated
/* 118 */ }
/* 119 */ columnartorow_batchIdx_0 = columnartorow_numRows_0;
/* 120 */ columnartorow_nextBatch_0();
/* 121 */ }
/* 122 */ // clean up resources
/* 123 */ if (columnartorow_mutableStateArray_1[0] != null) {
/* 124 */ columnartorow_mutableStateArray_1[0].close();
/* 125 */ }
/* 126 */
/* 127 */ }
/* 128 */
/* 129 */ protected void processNext() throws java.io.IOException {
/* 130 */ if (!sortAgg_noMoreInputTerm_0) {
/* 131 */ wholestagecodegen_doAggregateWithKeys_0(partitionIndex);
/* 132 */ if (!shouldStop()) {
/* 133 */ sortAgg_noMoreInputTerm_0 = true;
/* 134 */ if (sortAgg_initGroup_0) {
/* 135 */ sortAgg_doAggregateWithKeysOutput_0();
/* 136 */ }
/* 137 */ }
/* 138 */ }
/* 139 */ }
/* 140 */
/* 141 */ private void columnartorow_nextBatch_0() throws
java.io.IOException {
/* 142 */ columnartorow_mutableStateArray_1[0] =
org.apache.spark.sql.execution.ColumnarToRowExec.advanceBatch(
/* 143 */ columnartorow_mutableStateArray_0[0],
columnartorow_mutableStateArray_1[0],
((org.apache.spark.sql.execution.metric.SQLMetric) references[3] /*
numInputBatches */), ((org.apache.spark.sql.execution.metric.SQLMetric)
references[2] /* numOutputRows */));
/* 144 */ if (columnartorow_mutableStateArray_1[0] != null) {
/* 145 */ columnartorow_batchIdx_0 = 0;
/* 146 */ columnartorow_mutableStateArray_2[0] =
(org.apache.spark.sql.execution.vectorized.OnHeapColumnVector)
columnartorow_mutableStateArray_1[0].column(0);
/* 147 */
/* 148 */ }
/* 149 */ }
/* 150 */
/* 151 */ private void sortAgg_doAggregateWithKeysOutput_0() throws
java.io.IOException {
/* 152 */ ((org.apache.spark.sql.execution.metric.SQLMetric)
references[0] /* numOutputRows */).add(1);
/* 153 */
/* 154 */ boolean sortAgg_isNull_1 =
sortAgg_currentGroupingKey_0.isNullAt(0);
/* 155 */ long sortAgg_value_1 = sortAgg_isNull_1 ?
/* 156 */ -1L : (sortAgg_currentGroupingKey_0.getLong(0));
/* 157 */
/* 158 */ sortAgg_mutableStateArray_0[0].reset();
/* 159 */
/* 160 */ sortAgg_mutableStateArray_0[0].zeroOutNullBytes();
/* 161 */
/* 162 */ sortAgg_mutableStateArray_0[0].writeNullable(0,
sortAgg_value_1, sortAgg_isNull_1);
/* 163 */
/* 164 */ sortAgg_mutableStateArray_0[0].write(1, sortAgg_bufValue_0);
/* 165 */ append((sortAgg_mutableStateArray_0[0].getRow()));
/* 166 */
/* 167 */ }
/* 168 */
/* 169 */ }
```
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code
Closes #57153 from ulysses-you/SPARK-32750-sort-agg-codegen.
Authored-by: Xiduo You <[email protected]>
Signed-off-by: Xiduo You <[email protected]>
---
.../src/main/resources/error/error-conditions.json | 5 -
.../org/apache/spark/sql/internal/SQLConf.scala | 10 +
.../SortAggregateBenchmark-jdk21-results.txt | 72 +++++++
.../SortAggregateBenchmark-jdk25-results.txt | 72 +++++++
.../benchmarks/SortAggregateBenchmark-results.txt | 72 +++++++
.../aggregate/AggregateCodegenSupport.scala | 42 +++-
.../execution/aggregate/SortAggregateExec.scala | 218 ++++++++++++++++++++-
.../sql/execution/WholeStageCodegenSuite.scala | 209 +++++++++++++++++++-
.../benchmark/SortAggregateBenchmark.scala | 151 ++++++++++++++
9 files changed, 828 insertions(+), 23 deletions(-)
diff --git a/common/utils/src/main/resources/error/error-conditions.json
b/common/utils/src/main/resources/error/error-conditions.json
index 7d9f4cc7d6f1..6adcf5d23cb7 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -11633,11 +11633,6 @@
"AcceptsLatestSeenOffset is not supported with DSv1 streaming source:
<unsupportedSources>"
]
},
- "_LEGACY_ERROR_TEMP_3170" : {
- "message" : [
- "SortAggregate code-gen does not support grouping keys"
- ]
- },
"_LEGACY_ERROR_TEMP_3173" : {
"message" : [
"Cannot specify 'USING index_type' in 'CREATE INDEX'"
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
index 132d38faac7c..efa5b0352113 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
@@ -3797,6 +3797,16 @@ object SQLConf {
.booleanConf
.createWithDefault(true)
+ val ENABLE_SORT_AGGREGATE_CODEGEN_WITH_KEYS =
+ buildConf("spark.sql.codegen.aggregate.sortAggregate.withKeys.enabled")
+ .internal()
+ .doc("When true, enable code-gen for sort aggregate with grouping keys.
Takes effect only " +
+ s"when ${ENABLE_SORT_AGGREGATE_CODEGEN.key} is enabled.")
+ .version("4.3.0")
+ .withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE)
+ .booleanConf
+ .createWithDefault(true)
+
val ENABLE_FULL_OUTER_SHUFFLED_HASH_JOIN_CODEGEN =
buildConf("spark.sql.codegen.join.fullOuterShuffledHashJoin.enabled")
.internal()
diff --git a/sql/core/benchmarks/SortAggregateBenchmark-jdk21-results.txt
b/sql/core/benchmarks/SortAggregateBenchmark-jdk21-results.txt
new file mode 100644
index 000000000000..221ff18c190f
--- /dev/null
+++ b/sql/core/benchmarks/SortAggregateBenchmark-jdk21-results.txt
@@ -0,0 +1,72 @@
+================================================================================================
+sort aggregate without grouping
+================================================================================================
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+sort agg w/o group: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 16835 16940
149 31.1 32.1 1.0X
+codegen = T 444 456
10 1182.0 0.8 38.0X
+
+
+================================================================================================
+sort aggregate with linear keys
+================================================================================================
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+sort agg w linear keys: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 21718 22249
751 3.9 258.9 1.0X
+codegen = T 17500 17624
114 4.8 208.6 1.2X
+
+
+================================================================================================
+sort aggregate with randomized keys
+================================================================================================
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+sort agg w randomized keys: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 32644 32806
229 2.6 389.1 1.0X
+codegen = T 29288 29539
182 2.9 349.1 1.1X
+
+
+================================================================================================
+sort aggregate with string key
+================================================================================================
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+sort agg w string key: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 6726 6767
57 3.1 320.7 1.0X
+codegen = T 5885 5947
79 3.6 280.6 1.1X
+
+
+================================================================================================
+sort aggregate with decimal key
+================================================================================================
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+sort agg w decimal key: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 3371 3426
78 6.2 160.7 1.0X
+codegen = T 2530 2563
41 8.3 120.6 1.3X
+
+
+================================================================================================
+sort aggregate with multiple key types
+================================================================================================
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+sort agg w multiple keys: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 6586 6604
26 3.2 314.0 1.0X
+codegen = T 6035 6050
13 3.5 287.8 1.1X
+
+
diff --git a/sql/core/benchmarks/SortAggregateBenchmark-jdk25-results.txt
b/sql/core/benchmarks/SortAggregateBenchmark-jdk25-results.txt
new file mode 100644
index 000000000000..12cc5a0fe825
--- /dev/null
+++ b/sql/core/benchmarks/SortAggregateBenchmark-jdk25-results.txt
@@ -0,0 +1,72 @@
+================================================================================================
+sort aggregate without grouping
+================================================================================================
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w/o group: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 16207 17142
1323 32.4 30.9 1.0X
+codegen = T 415 422
5 1264.8 0.8 39.1X
+
+
+================================================================================================
+sort aggregate with linear keys
+================================================================================================
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w linear keys: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 19884 20041
222 4.2 237.0 1.0X
+codegen = T 15733 16010
468 5.3 187.6 1.3X
+
+
+================================================================================================
+sort aggregate with randomized keys
+================================================================================================
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w randomized keys: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 28860 28899
56 2.9 344.0 1.0X
+codegen = T 25706 25781
58 3.3 306.4 1.1X
+
+
+================================================================================================
+sort aggregate with string key
+================================================================================================
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w string key: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 6927 6934
11 3.0 330.3 1.0X
+codegen = T 5836 5902
45 3.6 278.3 1.2X
+
+
+================================================================================================
+sort aggregate with decimal key
+================================================================================================
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w decimal key: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 3182 3288
150 6.6 151.7 1.0X
+codegen = T 2390 2438
45 8.8 113.9 1.3X
+
+
+================================================================================================
+sort aggregate with multiple key types
+================================================================================================
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w multiple keys: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 6647 6662
21 3.2 317.0 1.0X
+codegen = T 6057 6080
27 3.5 288.8 1.1X
+
+
diff --git a/sql/core/benchmarks/SortAggregateBenchmark-results.txt
b/sql/core/benchmarks/SortAggregateBenchmark-results.txt
new file mode 100644
index 000000000000..422de3a6713f
--- /dev/null
+++ b/sql/core/benchmarks/SortAggregateBenchmark-results.txt
@@ -0,0 +1,72 @@
+================================================================================================
+sort aggregate without grouping
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w/o group: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 17331 17444
160 30.3 33.1 1.0X
+codegen = T 881 885
3 595.1 1.7 19.7X
+
+
+================================================================================================
+sort aggregate with linear keys
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w linear keys: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 20923 21112
267 4.0 249.4 1.0X
+codegen = T 17299 18455
680 4.8 206.2 1.2X
+
+
+================================================================================================
+sort aggregate with randomized keys
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w randomized keys: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 30049 30198
210 2.8 358.2 1.0X
+codegen = T 26696 26804
101 3.1 318.2 1.1X
+
+
+================================================================================================
+sort aggregate with string key
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w string key: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 6220 6264
63 3.4 296.6 1.0X
+codegen = T 5632 5682
95 3.7 268.6 1.1X
+
+
+================================================================================================
+sort aggregate with decimal key
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w decimal key: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 3220 3239
28 6.5 153.5 1.0X
+codegen = T 2482 2529
56 8.4 118.4 1.3X
+
+
+================================================================================================
+sort aggregate with multiple key types
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+sort agg w multiple keys: Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
+------------------------------------------------------------------------------------------------------------------------
+codegen = F 6378 6379
2 3.3 304.1 1.0X
+codegen = T 5660 5754
70 3.7 269.9 1.1X
+
+
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggregateCodegenSupport.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggregateCodegenSupport.scala
index 352388a6d8a1..884a8188a5ae 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggregateCodegenSupport.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggregateCodegenSupport.scala
@@ -45,9 +45,10 @@ trait AggregateCodegenSupport
/**
* The variables are used as aggregation buffers and each aggregate function
has one or more
- * ExprCode to initialize its buffer slots. Only used for aggregation
without keys.
+ * ExprCode to initialize its buffer slots. Used for aggregation without
keys, and for sort-based
+ * aggregation with keys (where a single group is aggregated at a time).
*/
- private var bufVars: Seq[Seq[ExprCode]] = _
+ protected var bufVars: Seq[Seq[ExprCode]] = _
/**
* Whether this operator needs to build hash table.
@@ -94,13 +95,13 @@ trait AggregateCodegenSupport
override def usedInputs: AttributeSet = inputSet
/**
- * The generated code for `doProduce` call when aggregate does not have
grouping keys.
+ * Creates global mutable state variables to hold the aggregation buffer,
one nested sequence of
+ * `ExprCode` per aggregate function. The returned `ExprCode`s carry the
code that (re)initializes
+ * the buffer slots with the aggregate functions' initial values; running
that code resets the
+ * buffer for a new group. The result is also stored in `bufVars` so that
`doConsume` can update
+ * the same variables.
*/
- private def doProduceWithoutKeys(ctx: CodegenContext): String = {
- val initAgg = ctx.addMutableState(CodeGenerator.JAVA_BOOLEAN, "initAgg")
- // The generated function doesn't have input row in the code context.
- ctx.INPUT_ROW = null
-
+ protected def createAggBufVars(ctx: CodegenContext): Seq[Seq[ExprCode]] = {
// generate variables for aggregation buffer
val functions =
aggregateExpressions.map(_.aggregateFunction.asInstanceOf[DeclarativeAggregate])
val initExpr = functions.map(f => f.initialValues)
@@ -121,9 +122,21 @@ trait AggregateCodegenSupport
JavaCode.global(value, e.dataType))
}
}
- val flatBufVars = bufVars.flatten
- val initBufVar = evaluateVariables(flatBufVars)
+ bufVars
+ }
+ /**
+ * The generated code for `doProduce` call when aggregate does not have
grouping keys.
+ */
+ private def doProduceWithoutKeys(ctx: CodegenContext): String = {
+ val initAgg = ctx.addMutableState(CodeGenerator.JAVA_BOOLEAN, "initAgg")
+ // The generated function doesn't have input row in the code context.
+ ctx.INPUT_ROW = null
+ // generate variables for aggregation buffer
+ val flatBufVars = createAggBufVars(ctx).flatten
+ val initBufVar = evaluateVariables(flatBufVars)
+ val functions =
+
aggregateExpressions.map(_.aggregateFunction.asInstanceOf[DeclarativeAggregate])
// generate variables for output
val (resultVars, genResult) = if (modes.contains(Final) ||
modes.contains(Complete)) {
// evaluate aggregate results
@@ -195,6 +208,15 @@ trait AggregateCodegenSupport
* The generated code for `doConsume` call when aggregate does not have
grouping keys.
*/
private def doConsumeWithoutKeys(ctx: CodegenContext, input: Seq[ExprCode]):
String = {
+ generateAggBufferUpdateCode(ctx, input)
+ }
+
+ /**
+ * The generated code that evaluates the aggregate functions for one input
row and updates the
+ * aggregation buffer held in `bufVars`. Shared by the no-keys path and the
sort-based with-keys
+ * path, both of which keep the aggregation buffer in global mutable state
variables.
+ */
+ protected def generateAggBufferUpdateCode(ctx: CodegenContext, input:
Seq[ExprCode]): String = {
// only have DeclarativeAggregate
val functions =
aggregateExpressions.map(_.aggregateFunction.asInstanceOf[DeclarativeAggregate])
val inputAttrs = functions.flatMap(_.aggBufferAttributes) ++
inputAttributes
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/SortAggregateExec.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/SortAggregateExec.scala
index 06f87af50eb5..4d01e2e7a716 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/SortAggregateExec.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/SortAggregateExec.scala
@@ -17,14 +17,15 @@
package org.apache.spark.sql.execution.aggregate
-import org.apache.spark.SparkUnsupportedOperationException
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions._
+import org.apache.spark.sql.catalyst.expressions.BindReferences.bindReferences
import org.apache.spark.sql.catalyst.expressions.aggregate._
-import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext,
ExprCode}
+import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext,
CodeGenerator, ExprCode, GenerateUnsafeProjection}
+import org.apache.spark.sql.catalyst.util.UnsafeRowUtils
import org.apache.spark.sql.catalyst.util.truncatedString
-import org.apache.spark.sql.execution.{OrderPreservingUnaryExecNode, SparkPlan}
+import org.apache.spark.sql.execution.{CodegenSupport,
OrderPreservingUnaryExecNode, SparkPlan}
import org.apache.spark.sql.execution.metric.SQLMetrics
import org.apache.spark.sql.internal.SQLConf
@@ -94,19 +95,222 @@ case class SortAggregateExec(
}
override def supportCodegen: Boolean = {
- // TODO(SPARK-32750): Support sort aggregate code-gen with grouping keys
super.supportCodegen &&
conf.getConf(SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN) &&
- groupingExpressions.isEmpty
+ (groupingExpressions.isEmpty || supportCodegenWithKeys)
+ }
+
+ private def supportCodegenWithKeys: Boolean = {
+ conf.getConf(SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN_WITH_KEYS) &&
+ groupingExpressions.forall(e =>
UnsafeRowUtils.isBinaryStable(e.dataType))
}
protected override def needHashTable: Boolean = false
+ // For the with-keys path, results are produced incrementally while scanning
the sorted input: a
+ // group's result is emitted (appended to the output buffer) as soon as the
next group starts. The
+ // child's producing loop must therefore honor `shouldStop()`, so it yields
right after a result
+ // row is buffered and resumes scanning on the next `processNext` call (see
`doProduceWithKeys`).
+ // Otherwise the child would scan the whole partition in one go, and every
emitted row would alias
+ // the single reused output row buffer. The with-keys path is thus not fully
blocking. The
+ // without-keys path produces its single result only after the scan
completes, so the blocking
+ // default (no stop check) still applies there.
+ //
+ // We keep the inherited `needCopyResult = false`, but its original
justification ("blocking
+ // operators keep the data in some buffer") no longer applies once the
with-keys path streams
+ // results out mid-scan. It stays safe for a different reason: the
stop-check discipline above
+ // guarantees at most one output row is buffered before it is consumed, so
the reused output row
+ // is never aliased across emitted rows, and any in-stage parent that
multiplies rows (e.g. a
+ // join) declares `needCopyResult = true` itself.
+ override def needStopCheck: Boolean = groupingExpressions.nonEmpty
+
+ override protected def canCheckLimitNotReached: Boolean =
groupingExpressions.nonEmpty
+
+ // The global UnsafeRow holding the grouping key of the group currently
being aggregated.
+ private var currentGroupingKeyTerm: String = _
+
+ // The global boolean flag indicating whether the current group has been
started, i.e. at least
+ // one input row has been processed.
+ private var initGroupTerm: String = _
+
+ // The code that (re)initializes the aggregation buffer variables to the
initial values of the
+ // aggregate functions. Used to reset the buffer when a new group starts.
+ private var reInitBufferCode: String = _
+
+ // The name of the generated function that outputs the result of the current
group.
+ private var outputFuncName: String = _
+
+ /**
+ * Generate the code for output. The aggregation buffer is held in the
global `bufVars` and the
+ * grouping key in `currentGroupingKeyTerm`, both populated while scanning
the current group.
+ * @return function name for the result code.
+ */
+ private def generateResultFunctionForKeys(ctx: CodegenContext): String = {
+ val funcName = ctx.freshName("doAggregateWithKeysOutput")
+ val numOutput = metricTerm(ctx, "numOutputRows")
+ val flatBufVars = bufVars.flatten
+ val groupingAttributes = groupingExpressions.map(_.toAttribute)
+
+ val body =
+ if (modes.contains(Final) || modes.contains(Complete)) {
+ // generate output using resultExpressions
+ ctx.currentVars = null
+ ctx.INPUT_ROW = currentGroupingKeyTerm
+ val keyVars = groupingExpressions.zipWithIndex.map { case (e, i) =>
+ BoundReference(i, e.dataType, e.nullable).genCode(ctx)
+ }
+ val evaluateKeyVars = evaluateVariables(keyVars)
+ // evaluate the aggregation result from the buffer variables
+ ctx.currentVars = flatBufVars
+ ctx.INPUT_ROW = null
+ val functions =
+
aggregateExpressions.map(_.aggregateFunction.asInstanceOf[DeclarativeAggregate])
+ val aggResults = bindReferences(
+ functions.map(_.evaluateExpression),
+ aggregateBufferAttributes).map(_.genCode(ctx))
+ val evaluateAggResults = evaluateVariables(aggResults)
+ // generate the final result
+ ctx.currentVars = keyVars ++ aggResults
+ val inputAttrs = groupingAttributes ++ aggregateAttributes
+ val resultVars = bindReferences[Expression](
+ resultExpressions,
+ inputAttrs).map(_.genCode(ctx))
+ val evaluateNondeterministicResults =
+ evaluateNondeterministicVariables(output, resultVars,
resultExpressions)
+ s"""
+ |$evaluateKeyVars
+ |$evaluateAggResults
+ |$evaluateNondeterministicResults
+ |${consume(ctx, resultVars)}
+ """.stripMargin
+ } else if (modes.contains(Partial) || modes.contains(PartialMerge)) {
+ // resultExpressions are Attributes of groupingExpressions and
aggregateBufferAttributes.
+ assert(resultExpressions.forall(_.isInstanceOf[Attribute]))
+ assert(resultExpressions.length ==
+ groupingExpressions.length + aggregateBufferAttributes.length)
+
+ ctx.currentVars = null
+ ctx.INPUT_ROW = currentGroupingKeyTerm
+ val keyVars = groupingExpressions.zipWithIndex.map { case (e, i) =>
+ BoundReference(i, e.dataType, e.nullable).genCode(ctx)
+ }
+ val evaluateKeyVars = evaluateVariables(keyVars)
+
+ // the aggregation buffer values are output directly
+ ctx.currentVars = keyVars ++ flatBufVars
+ ctx.INPUT_ROW = null
+ val inputAttrs = resultExpressions.map(_.toAttribute)
+ val resultVars = bindReferences[Expression](
+ resultExpressions,
+ inputAttrs).map(_.genCode(ctx))
+ s"""
+ |$evaluateKeyVars
+ |${consume(ctx, resultVars)}
+ """.stripMargin
+ } else {
+ // generate result based on grouping key
+ ctx.INPUT_ROW = currentGroupingKeyTerm
+ ctx.currentVars = null
+ val resultVars = bindReferences[Expression](
+ resultExpressions,
+ groupingAttributes).map(_.genCode(ctx))
+ val evaluateNondeterministicResults =
+ evaluateNondeterministicVariables(output, resultVars,
resultExpressions)
+ s"""
+ |$evaluateNondeterministicResults
+ |${consume(ctx, resultVars)}
+ """.stripMargin
+ }
+ ctx.addNewFunction(funcName,
+ s"""
+ |private void $funcName() throws java.io.IOException {
+ | $numOutput.add(1);
+ | $body
+ |}
+ """.stripMargin)
+ }
+
protected override def doProduceWithKeys(ctx: CodegenContext): String = {
- throw new SparkUnsupportedOperationException("_LEGACY_ERROR_TEMP_3170")
+ ctx.INPUT_ROW = null
+ // Generate the global variables for the aggregation buffer of the current
group. Capture the
+ // buffer initialization code (and clear it from `bufVars`, so the
result/update code does not
+ // re-emit it); it is reused in `doConsumeWithKeys` to reset the buffer
when a new group starts.
+ reInitBufferCode = evaluateVariables(createAggBufVars(ctx).flatten)
+ // Global state to track the current group. Inline the grouping-key row
(rather than letting it
+ // be compacted into a shared array) so its term is a plain variable name:
it is used as
+ // `ctx.INPUT_ROW` when generating the output, and an array-subscript term
there would break the
+ // generated code when key expressions are extracted into split functions.
+ currentGroupingKeyTerm =
+ ctx.addMutableState("UnsafeRow", "currentGroupingKey", forceInline =
true)
+ initGroupTerm = ctx.addMutableState(CodeGenerator.JAVA_BOOLEAN,
"initGroup")
+ // Whether the whole sorted input has been consumed.
+ val noMoreInputTerm = ctx.addMutableState(CodeGenerator.JAVA_BOOLEAN,
"noMoreInputTerm")
+ // Generate the output function before `child.produce`, so that
`doConsumeWithKeys` can call it
+ // when it detects a group boundary.
+ outputFuncName = generateResultFunctionForKeys(ctx)
+
+ val doAgg = ctx.freshName("doAggregateWithKeys")
+ // Pass `partitionIndex` as a parameter so bare references in the child's
+ // produce resolve to the local, not the protected superclass field.
+ // Required when `addNewFunction` spills this helper to a nested class.
+ val doAggFuncName = ctx.addNewFunction(doAgg,
+ s"""
+ |private void $doAgg(int partitionIndex) throws java.io.IOException {
+ | ${child.asInstanceOf[CodegenSupport].produce(ctx, this)}
+ |}
+ """.stripMargin)
+
+ // Sort-based aggregation consumes the sorted input row by row, emitting a
group's result as
+ // soon as the next group starts (see `doConsumeWithKeys`). Emitting a row
appends to the output
+ // buffer, which makes the child's `shouldStop()` return true, so the
child's producing loop
+ // returns to here mid-scan. We therefore must be able to resume scanning
across multiple
+ // `processNext` invocations:
+ // - `$noMoreInputTerm` guards against re-running once the input is fully
consumed;
+ // - after `$doAggFuncName()` returns, `shouldStop()` distinguishes a
real end-of-input (the
+ // child's loop exhausted, so `shouldStop()` is false) from a mid-scan
pause (`shouldStop()`
+ // is true because an output row is buffered). Only on a real
end-of-input do we mark the
+ // scan done and flush the last group. If the last input row also
triggered an output (so
+ // `shouldStop()` is still true at exhaustion), the next `processNext`
re-enters, the child's
+ // loop produces nothing, `shouldStop()` is then false, and the last
group is flushed.
+ s"""
+ |if (!$noMoreInputTerm) {
+ | $doAggFuncName(partitionIndex);
+ | if (!shouldStop()) {
+ | $noMoreInputTerm = true;
+ | if ($initGroupTerm) {
+ | $outputFuncName();
+ | }
+ | }
+ |}
+ """.stripMargin
}
protected override def doConsumeWithKeys(ctx: CodegenContext, input:
Seq[ExprCode]): String = {
- throw new SparkUnsupportedOperationException("_LEGACY_ERROR_TEMP_3170")
+ // Create the grouping key. `ctx.currentVars` is still set to `input` here.
+ val groupingUnsafeRowKeyCode = GenerateUnsafeProjection.createCode(
+ ctx, bindReferences[Expression](groupingExpressions, child.output))
+ val groupingUnsafeRowKey = groupingUnsafeRowKeyCode.value
+ // The code to update the aggregation buffer with the current input row.
+ val updateBufferCode = generateAggBufferUpdateCode(ctx, input)
+
+ // `reInitBufferCode` was captured in `doProduceWithKeys`; it
(re)initializes the aggregation
+ // buffer variables to the aggregate functions' initial values, resetting
them for a new group.
+ // The input is sorted by the grouping key, so rows of the same group are
contiguous. When the
+ // grouping key changes, the current group is complete: output it and
reset the buffer for the
+ // new group. Group equality uses the binary representation of the key,
which is valid because
+ // `supportCodegen` restricts grouping keys to binary-stable types.
+ s"""
+ |${groupingUnsafeRowKeyCode.code}
+ |if (!$initGroupTerm) {
+ | $initGroupTerm = true;
+ | $currentGroupingKeyTerm = $groupingUnsafeRowKey.copy();
+ | $reInitBufferCode
+ |} else if (!$currentGroupingKeyTerm.equals($groupingUnsafeRowKey)) {
+ | $outputFuncName();
+ | $currentGroupingKeyTerm = $groupingUnsafeRowKey.copy();
+ | $reInitBufferCode
+ |}
+ |$updateBufferCode
+ """.stripMargin
}
override def simpleString(maxFields: Int): String = toString(verbose =
false, maxFields)
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala
index bcd2f5369932..2e73417dd9ed 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala
@@ -33,7 +33,7 @@ import
org.apache.spark.sql.execution.joins.{BroadcastHashJoinExec, BroadcastNes
import org.apache.spark.sql.functions._
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.test.SharedSparkSession
-import org.apache.spark.sql.types.{DayTimeIntervalType, DecimalType,
IntegerType, StringType, StructField, StructType}
+import org.apache.spark.sql.types.{DayTimeIntervalType, DecimalType,
DoubleType, FloatType, IntegerType, LongType, StringType, StructField,
StructType}
// Disable AQE because the WholeStageCodegenExec is added when running
QueryStageExec
class WholeStageCodegenSuite extends SharedSparkSession
@@ -68,6 +68,213 @@ class WholeStageCodegenSuite extends SharedSparkSession
}
}
+ // Runs `query` on `data` with sort aggregate forced and its code-gen
enabled, asserts the plan
+ // actually uses a code-gen'd SortAggregateExec, and checks the result
matches the interpreted
+ // (code-gen disabled) result.
+ private def checkSortAggregateCodegen(
+ data: Dataset[Row])(query: Dataset[Row] => Dataset[Row]): Unit = {
+ // Disable both hash-based aggregate operators so the planner always picks
SortAggregateExec.
+ val forceSortAggregate = Seq(
+ SQLConf.USE_HASH_AGG.key -> "false",
+ SQLConf.USE_OBJECT_HASH_AGG.key -> "false")
+ val expected = withSQLConf(
+ (forceSortAggregate :+ (SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN.key ->
"false")): _*) {
+ val df = query(data)
+ assert(!df.queryExecution.executedPlan.exists(p =>
+ p.isInstanceOf[WholeStageCodegenExec] &&
+
p.asInstanceOf[WholeStageCodegenExec].child.isInstanceOf[SortAggregateExec]),
+ s"Expected no code-gen'd SortAggregateExec
in:\n${df.queryExecution.executedPlan}")
+ df.collect()
+ }
+ withSQLConf(
+ (forceSortAggregate :+ (SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN.key ->
"true")): _*) {
+ val df = query(data)
+ assert(df.queryExecution.executedPlan.exists(p =>
+ p.isInstanceOf[WholeStageCodegenExec] &&
+
p.asInstanceOf[WholeStageCodegenExec].child.isInstanceOf[SortAggregateExec]),
+ s"Expected a code-gen'd SortAggregateExec
in:\n${df.queryExecution.executedPlan}")
+ checkAnswer(df, expected)
+ }
+ }
+
+ test("SPARK-32750: SortAggregate code-gen with grouping keys") {
+ val data = spark.range(200).selectExpr(
+ "id",
+ "id % 7 as k1",
+ "id % 3 as k2",
+ "case when id % 5 = 0 then null else id end as v",
+ "case when id % 4 = 0 then null else cast(id % 11 as string) end as s")
+
+ // Exercise a variety of shapes: multiple/numeric/string grouping keys,
null keys and values,
+ // single-row groups, a single all-rows group, and a downstream limit
(which exercises the
+ // resumable `shouldStop` path in the generated produce loop).
+ checkSortAggregateCodegen(data) {
+ _.groupBy("k1", "k2")
+ .agg(count(col("v")), sum(col("v")), max(col("v")), min(col("v")))
+ .orderBy("k1", "k2")
+ }
+ // expression grouping keys, aggregates over expressions, and arithmetic
on the results
+ checkSortAggregateCodegen(data) {
+ _.groupBy((col("k1") + col("k2")).as("k"))
+ .agg(
+ (sum(col("v") * lit(2)) + lit(1)).as("weighted"),
+ (max(col("v")) - min(col("v"))).as("spread"),
+ count(col("v")).as("cnt"))
+ .orderBy("k")
+ }
+ // aggregates with FILTER (WHERE) clauses
+ checkSortAggregateCodegen(data) {
+ _.groupBy("k1")
+ .agg(
+ expr("sum(v) FILTER (WHERE v > 50)"),
+ expr("count(v) FILTER (WHERE s IS NOT NULL)"),
+ avg(col("v")))
+ .orderBy("k1")
+ }
+ // string grouping key with nulls, followed by a HAVING-style filter on
the aggregate
+ checkSortAggregateCodegen(data) {
+ _.groupBy("s").agg(count(col("v")).as("cnt"), sum(col("v")).as("total"))
+ .where(col("cnt") > 2)
+ .orderBy("s")
+ }
+ // count(distinct ...): rewritten to a two-round aggregation, both of
which are sort aggregates
+ checkSortAggregateCodegen(data) {
+ _.groupBy("k2").agg(countDistinct(col("v")), sum(col("v"))).orderBy("k2")
+ }
+ // every row is its own group
+
checkSortAggregateCodegen(data)(_.groupBy("id").agg(max(col("v"))).orderBy("id"))
+ // a single group covering all rows
+ checkSortAggregateCodegen(data)(_.groupBy(lit(1)).agg(sum(col("v")),
avg(col("v"))))
+ // downstream limit on top of the aggregate
+
checkSortAggregateCodegen(data)(_.groupBy("k1").agg(sum(col("v"))).orderBy("k1").limit(3))
+ }
+
+ test("SPARK-32750: SortAggregate code-gen with grouping keys - empty input")
{
+ // No rows: a grouped aggregate over empty input must produce no output
rows.
+ val data = spark.range(0).selectExpr("id", "id % 3 as k", "id as v")
+ checkSortAggregateCodegen(data)(_.groupBy("k").agg(sum(col("v")),
count(col("v"))).orderBy("k"))
+ }
+
+ test("SPARK-32750: SortAggregate code-gen with grouping keys - single
partition") {
+ // Force a single partition so all groups are produced within one task's
scan.
+ val data = spark.range(50).repartition(1).selectExpr("id", "id % 4 as k",
"id as v")
+ checkSortAggregateCodegen(data) {
+ _.groupBy((col("k") + lit(1)).as("k"))
+ .agg(
+ (sum(col("v")) + max(col("v"))).as("mixed"),
+ avg(col("v") * col("v")).as("avg_sq"),
+ expr("count(v) FILTER (WHERE v % 2 = 0)").as("evens"))
+ .orderBy("k")
+ }
+ }
+
+ test("SPARK-32750: SortAggregate code-gen with float/double grouping keys") {
+ // Float/double are binary-stable so they take the with-keys code-gen
path, and group
+ // boundaries are detected via UnsafeRow.equals. Cover -0.0 (which must
group with 0.0) and
+ // NaN (all NaNs must group together), whose canonicalization relies on
the planner's
+ // NormalizeFloatingNumbers rule running before the keys reach UnsafeRow.
+ val rows = Seq(
+ Row(0.0d, 0.0f, 1L),
+ Row(-0.0d, -0.0f, 2L),
+ Row(Double.NaN, Float.NaN, 3L),
+ Row(java.lang.Double.longBitsToDouble(0x7ff8000000000001L),
+ java.lang.Float.intBitsToFloat(0x7fc00001), 4L),
+ Row(1.5d, 1.5f, 5L),
+ Row(1.5d, 1.5f, 6L),
+ Row(null, null, 7L))
+ val schema = StructType(Seq(
+ StructField("d", DoubleType),
+ StructField("f", FloatType),
+ StructField("v", LongType)))
+ val data = spark.createDataFrame(spark.sparkContext.parallelize(rows),
schema)
+ checkSortAggregateCodegen(data) {
+ _.groupBy("d").agg(sum(col("v")), count(col("v"))).orderBy("d")
+ }
+ checkSortAggregateCodegen(data) {
+ _.groupBy("f").agg(sum(col("v")), count(col("v"))).orderBy("f")
+ }
+ checkSortAggregateCodegen(data) {
+ _.groupBy("d", "f").agg(sum(col("v"))).orderBy("d", "f")
+ }
+ }
+
+ test("SPARK-32750: SortAggregate code-gen with decimal grouping keys") {
+ // Decimal is binary-stable and takes the with-keys code-gen path. Include
nulls.
+ val data = spark.range(200).selectExpr(
+ "id",
+ "cast(id % 5 as decimal(10, 2)) as k",
+ "case when id % 6 = 0 then null else cast(id as decimal(20, 4)) end as
v")
+ checkSortAggregateCodegen(data) {
+ _.groupBy("k").agg(sum(col("v")), count(col("v")),
max(col("v"))).orderBy("k")
+ }
+ }
+
+ test("SPARK-32750: SortAggregate code-gen with grouping keys - no aggregate
functions") {
+ // Grouping-only aggregate (DISTINCT lowers to a grouping aggregate with
no aggregate
+ // functions), exercising the empty-buffer branch of the result code
generation.
+ val data = spark.range(200).selectExpr("id % 7 as k1", "id % 3 as k2")
+ checkSortAggregateCodegen(data)(_.select("k1").distinct().orderBy("k1"))
+ checkSortAggregateCodegen(data)(_.select("k1",
"k2").distinct().orderBy("k1", "k2"))
+ }
+
+ test("SPARK-32750: SortAggregate code-gen with grouping keys - split
aggregate functions") {
+ // Force the aggregate functions into separate split methods so the
with-keys path is
+ // exercised with split buffer-update code.
+ val data = spark.range(200).selectExpr("id", "id % 7 as k", "id as v")
+ withSQLConf(
+ SQLConf.CODEGEN_SPLIT_AGGREGATE_FUNC.key -> "true",
+ SQLConf.CODEGEN_METHOD_SPLIT_THRESHOLD.key -> "1") {
+ checkSortAggregateCodegen(data) {
+ _.groupBy("k")
+ .agg(sum(col("v")), count(col("v")), max(col("v")), min(col("v")),
avg(col("v")))
+ .orderBy("k")
+ }
+ }
+ }
+
+ test("SPARK-32750: SortAggregate code-gen with grouping keys - config gate")
{
+ // When the with-keys config is disabled, the grouped SortAggregate must
not be code-gen'd,
+ // while the result stays correct.
+ val data = spark.range(200).selectExpr("id % 7 as k", "id as v")
+ withSQLConf(
+ SQLConf.USE_HASH_AGG.key -> "false",
+ SQLConf.USE_OBJECT_HASH_AGG.key -> "false",
+ SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN.key -> "true",
+ SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN_WITH_KEYS.key -> "false") {
+ val df = data.groupBy("k").agg(sum(col("v"))).orderBy("k")
+ assert(!df.queryExecution.executedPlan.exists(p =>
+ p.isInstanceOf[WholeStageCodegenExec] &&
+
p.asInstanceOf[WholeStageCodegenExec].child.isInstanceOf[SortAggregateExec]),
+ s"Expected no code-gen'd SortAggregateExec
in:\n${df.queryExecution.executedPlan}")
+ checkAnswer(df, Seq(0, 1, 2, 3, 4, 5, 6).map { k =>
+ Row(k, (k until 200 by 7).map(_.toLong).sum)
+ })
+ }
+ // With the gate enabled it is code-gen'd (baseline for the assertion
above).
+
checkSortAggregateCodegen(data)(_.groupBy("k").agg(sum(col("v"))).orderBy("k"))
+ }
+
+ test("SPARK-32750: SortAggregate code-gen with grouping keys - non-binary
collated key") {
+ // A non-binary collation (e.g. UTF8_LCASE) is not binary-stable, so
`supportCodegenWithKeys`
+ // returns false and the grouped SortAggregate must fall back to the
interpreted path rather
+ // than being code-gen'd, while the collation-aware grouping result stays
correct.
+ val data = Seq("a", "A", "b", "B", "c")
+ .toDF("k").selectExpr("k collate UTF8_LCASE as k")
+ withSQLConf(
+ SQLConf.USE_HASH_AGG.key -> "false",
+ SQLConf.USE_OBJECT_HASH_AGG.key -> "false",
+ SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN.key -> "true",
+ SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN_WITH_KEYS.key -> "true") {
+ val df = data.groupBy("k").agg(count(lit(1)).as("cnt")).orderBy("cnt",
"k")
+ assert(!df.queryExecution.executedPlan.exists(p =>
+ p.isInstanceOf[WholeStageCodegenExec] &&
+
p.asInstanceOf[WholeStageCodegenExec].child.isInstanceOf[SortAggregateExec]),
+ s"Expected no code-gen'd SortAggregateExec
in:\n${df.queryExecution.executedPlan}")
+ // Under UTF8_LCASE, 'a'/'A' and 'b'/'B' each collapse to one group of
2, 'c' stays alone.
+ checkAnswer(df.select("cnt"), Seq(Row(1), Row(2), Row(2)))
+ }
+ }
+
testWithWholeStageCodegenOnAndOff("GenerateExec should be" +
" included in WholeStageCodegen") { codegenEnabled =>
import testImplicits._
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/benchmark/SortAggregateBenchmark.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/benchmark/SortAggregateBenchmark.scala
new file mode 100644
index 000000000000..64928c239b7c
--- /dev/null
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/benchmark/SortAggregateBenchmark.scala
@@ -0,0 +1,151 @@
+/*
+ * 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.spark.sql.execution.benchmark
+
+import org.apache.spark.benchmark.Benchmark
+import org.apache.spark.sql.internal.SQLConf
+
+/**
+ * Benchmark to measure performance for sort-based aggregate, focusing on the
whole-stage
+ * code-gen path. Hash-map based aggregation is intentionally excluded; both
+ * `spark.sql.execution.useHashAggregateExec` and
`spark.sql.execution.useObjectHashAggregateExec`
+ * are disabled so the planner always picks
+ * [[org.apache.spark.sql.execution.aggregate.SortAggregateExec]].
+ *
+ * To run this benchmark:
+ * {{{
+ * 1. without sbt: bin/spark-submit --class <this class>
+ * --jars <spark core test jar>,<spark catalyst test jar> <spark sql test
jar>
+ * 2. build/sbt "sql/Test/runMain <this class>"
+ * 3. generate result: SPARK_GENERATE_BENCHMARK_FILES=1 build/sbt
"sql/Test/runMain <this class>"
+ * Results will be written to
"benchmarks/SortAggregateBenchmark-results.txt".
+ * }}}
+ */
+object SortAggregateBenchmark extends SqlBasedBenchmark {
+
+ // Force the planner to pick SortAggregateExec by disabling both hash-based
aggregate operators.
+ private val forceSortAggregate: Map[String, String] = Map(
+ SQLConf.USE_HASH_AGG.key -> "false",
+ SQLConf.USE_OBJECT_HASH_AGG.key -> "false")
+
+ /**
+ * Adds the two cases we care about for a sort aggregate. Whole-stage
code-gen stays enabled in
+ * both so the child pipeline (scan, sort) is code-gen'd either way; only
the sort aggregate's own
+ * code-gen is toggled via `ENABLE_SORT_AGGREGATE_CODEGEN`, which isolates
its contribution:
+ * - code-gen off: sort aggregate falls back to the interpreted
`SortBasedAggregationIterator`;
+ * - code-gen on: code-gen'd `SortAggregateExec`.
+ */
+ private def addGroupingKeyCases(benchmark: Benchmark)(f: () => Unit): Unit =
{
+ benchmark.addCase("codegen = F", numIters = 2) { _ =>
+ withSQLConf(
+ (forceSortAggregate ++ Map(
+ SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> "true",
+ SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN.key -> "false")).toSeq: _*) {
+ f()
+ }
+ }
+
+ benchmark.addCase("codegen = T", numIters = 5) { _ =>
+ withSQLConf(
+ (forceSortAggregate ++ Map(
+ SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> "true",
+ SQLConf.ENABLE_SORT_AGGREGATE_CODEGEN.key -> "true")).toSeq: _*) {
+ f()
+ }
+ }
+ }
+
+ override def runBenchmarkSuite(mainArgs: Array[String]): Unit = {
+ runBenchmark("sort aggregate without grouping") {
+ val N = 500L << 20
+ val benchmark = new Benchmark("sort agg w/o group", N, output = output)
+ addGroupingKeyCases(benchmark) { () =>
+ spark.range(N).selectExpr("sum(id)").noop()
+ }
+ benchmark.run()
+ }
+
+ runBenchmark("sort aggregate with linear keys") {
+ val N = 20 << 22
+ val benchmark = new Benchmark("sort agg w linear keys", N, output =
output)
+ // The child of a sort aggregate must be sorted by the grouping keys.
The temp view is lazy,
+ // so the sort re-executes on every iteration (and without the explicit
`sortWithinPartitions`
+ // the planner would insert an equivalent `SortExec` below the aggregate
anyway); pinning the
+ // sort here just makes both cases pay the same sort cost, isolating the
aggregate's own
+ // contribution. This means the reported speedup understates the
aggregate-only gain.
+ spark.range(N).selectExpr("id", "(id & 65535) as k")
+ .sortWithinPartitions("k").createOrReplaceTempView("linear")
+ addGroupingKeyCases(benchmark) { () =>
+ spark.sql("select k, count(*), sum(id), max(id) from linear group by
k").noop()
+ }
+ benchmark.run()
+ }
+
+ runBenchmark("sort aggregate with randomized keys") {
+ val N = 20 << 22
+ val benchmark = new Benchmark("sort agg w randomized keys", N, output =
output)
+ spark.range(N).selectExpr("id", "floor(rand() * 10000) as k")
+ .sortWithinPartitions("k").createOrReplaceTempView("rand_keys")
+ addGroupingKeyCases(benchmark) { () =>
+ spark.sql("select k, count(*), sum(id), max(id) from rand_keys group
by k").noop()
+ }
+ benchmark.run()
+ }
+
+ runBenchmark("sort aggregate with string key") {
+ val N = 20 << 20
+ val benchmark = new Benchmark("sort agg w string key", N, output =
output)
+ spark.range(N).selectExpr("id", "cast(id & 1023 as string) as k")
+ .sortWithinPartitions("k").createOrReplaceTempView("string_key")
+ addGroupingKeyCases(benchmark) { () =>
+ spark.sql("select k, count(*), sum(id), max(id) from string_key group
by k").noop()
+ }
+ benchmark.run()
+ }
+
+ runBenchmark("sort aggregate with decimal key") {
+ val N = 20 << 20
+ val benchmark = new Benchmark("sort agg w decimal key", N, output =
output)
+ spark.range(N).selectExpr("id", "cast(id & 65535 as decimal(18, 0)) as
k")
+ .sortWithinPartitions("k").createOrReplaceTempView("decimal_key")
+ addGroupingKeyCases(benchmark) { () =>
+ spark.sql("select k, count(*), sum(id), max(id) from decimal_key group
by k").noop()
+ }
+ benchmark.run()
+ }
+
+ runBenchmark("sort aggregate with multiple key types") {
+ val N = 20 << 20
+ val benchmark = new Benchmark("sort agg w multiple keys", N, output =
output)
+ spark.range(N)
+ .selectExpr(
+ "id",
+ "(id & 1023) as k1",
+ "cast(id & 1023 as string) as k2",
+ "cast(id & 1023 as int) as k3",
+ "id > 1023 as k4")
+ .sortWithinPartitions("k1", "k2", "k3", "k4")
+ .createOrReplaceTempView("multi_keys")
+ addGroupingKeyCases(benchmark) { () =>
+ spark.sql("select k1, k2, k3, k4, count(*), sum(id), max(id) " +
+ "from multi_keys group by k1, k2, k3, k4").noop()
+ }
+ benchmark.run()
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]