Repository navigation
perf: let a partial aggregate skip aggregation for AVG #7543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -334,12 +334,33 @@ where | |
| ]) | ||
| } | ||
|
|
||
| /// Each row's own partial state, as `state` emits it: the value and a count of one, or a | ||
| /// zero sum and count for a null or filtered-out row. | ||
| fn convert_to_state( | ||
| &self, | ||
| _values: &[ArrayRef], | ||
| _opt_filter: Option<&arrow::array::BooleanArray>, | ||
| values: &[ArrayRef], | ||
| opt_filter: Option<&arrow::array::BooleanArray>, | ||
| ) -> Result<Vec<ArrayRef>> { | ||
| not_impl_err!("Input batch conversion to state not implemented") | ||
| let values = values[0].as_primitive::<T>(); | ||
| let mut sums = Vec::with_capacity(values.len()); | ||
| let mut counts = Vec::with_capacity(values.len()); | ||
| for idx in 0..values.len() { | ||
|
Comment on lines
+344
to
+347
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once skipping starts, every remaining row of the task goes through this loop, one |
||
| let included = values.is_valid(idx) | ||
| && opt_filter.is_none_or(|filter| filter.is_valid(idx) && filter.value(idx)); | ||
| if included { | ||
| sums.push(values.value(idx)); | ||
| counts.push(1); | ||
| } else { | ||
| sums.push(T::default_value()); | ||
| counts.push(0); | ||
| } | ||
| } | ||
| let sums = PrimitiveArray::<T>::new(sums.into(), None) | ||
| .with_data_type(self.return_data_type.clone()); | ||
| Ok(vec![ | ||
| Arc::new(sums) as ArrayRef, | ||
| Arc::new(Int64Array::new(counts.into(), None)) as ArrayRef, | ||
| ]) | ||
| } | ||
|
|
||
| fn size(&self) -> usize { | ||
|
|
@@ -365,4 +386,32 @@ mod tests { | |
| assert_eq!(partial.evaluate()?, ScalarValue::Float64(None)); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Converting rows straight to partial states, as a partial aggregate that stops aggregating | ||
| /// does, gives each row the state that aggregating it alone into a group would. | ||
| #[test] | ||
| fn convert_to_state_matches_one_row_groups() -> Result<()> { | ||
| use arrow::array::BooleanArray; | ||
| let values: ArrayRef = Arc::new(Float64Array::from(vec![ | ||
| Some(1.5), | ||
| None, | ||
| Some(-2.0), | ||
| Some(4.0), | ||
| ])); | ||
| let filter = BooleanArray::from(vec![Some(true), Some(true), Some(true), Some(false)]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The filter here has no null slot, so the |
||
| let new_accumulator = || { | ||
| AvgGroupsAccumulator::<arrow::datatypes::Float64Type, _>::new( | ||
| &Float64, | ||
| |sum: f64, count: i64| Ok(sum / count as f64), | ||
| ) | ||
| }; | ||
|
|
||
| let converted = | ||
| new_accumulator().convert_to_state(&[Arc::clone(&values)], Some(&filter))?; | ||
|
|
||
| let mut grouped = new_accumulator(); | ||
| grouped.update_batch(&[values], &[0, 1, 2, 3], Some(&filter), 4)?; | ||
| assert_eq!(converted, grouped.state(EmitTo::All)?); | ||
| Ok(()) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3431,6 +3431,59 @@ class CometAggregateSuite extends CometTestBase with AdaptiveSparkPlanHelper { | |
| } | ||
| } | ||
|
|
||
| test("skip partial aggregation converts AVG rows to partial states") { | ||
| withTempDir { dir => | ||
| val path = new Path(dir.toURI.toString, "input").toUri.toString | ||
| withSQLConf(CometConf.COMET_ENABLED.key -> "false") { | ||
| // Mostly distinct keys within each task, with some keys repeating across tasks so the | ||
| // final aggregate merges converted states with each other. Values include nulls. | ||
| spark | ||
| .range(0L, 16384L, 1L, 8) | ||
| .selectExpr( | ||
| "id % 12000 AS k", | ||
| "CASE WHEN id % 5 = 0 THEN NULL ELSE CAST(id % 17 AS INT) END AS i", | ||
| "CASE WHEN id % 7 = 0 THEN NULL ELSE CAST(id % 11 AS DOUBLE) / 3 END AS d", | ||
| "CASE WHEN id % 3 = 0 THEN NULL ELSE CAST(id % 13 AS DECIMAL(10, 2)) / 7 END AS small", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| "CASE WHEN id % 4 = 0 THEN NULL ELSE CAST(id % 19 AS DECIMAL(38, 10)) / 7 END AS big") | ||
| .write | ||
| .parquet(path) | ||
| } | ||
| withParquetTable(path, "skip_partial_avg") { | ||
| withSQLConf( | ||
| SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false", | ||
| SQLConf.SHUFFLE_PARTITIONS.key -> "8", | ||
| CometConf.COMET_BATCH_SIZE.key -> "128", | ||
| CometConf.COMET_SHUFFLE_ENABLED.key -> "true", | ||
| CometConf.COMET_SHUFFLE_MODE.key -> "native", | ||
| CometConf.COMET_EXEC_AGGREGATE_SKIP_PARTIAL_ENABLED.key -> "true", | ||
| CometConf.COMET_RESPECT_DATAFUSION_CONFIGS.key -> "true", | ||
| "spark.comet.datafusion.execution.skip_partial_aggregation_probe_rows_threshold" -> "100", | ||
| "spark.comet.datafusion.execution.skip_partial_aggregation_probe_ratio_threshold" -> "0.8") { | ||
| def skipped(query: String): Long = { | ||
| val (_, plan) = checkSparkAnswerAndOperator(query) | ||
| stripAQEPlan(plan).collect { case aggregate: CometHashAggregateExec => | ||
| aggregate.metrics.get("skipped_aggregation_rows").map(_.value).getOrElse(0L) | ||
| }.sum | ||
| } | ||
| def avgQuery(expressions: String): String = | ||
| s"SELECT k, $expressions FROM skip_partial_avg GROUP BY k" | ||
|
|
||
| // AVG over integral and floating-point inputs stops aggregating. | ||
| for (expressions <- Seq("avg(i)", "avg(d)", "avg(i), avg(d), count(i)")) { | ||
| assert(skipped(avgQuery(expressions)) > 0L, s"Expected skipping for $expressions") | ||
| } | ||
| // AVG over a decimal input that Comet aggregates as a decimal cannot convert rows to | ||
| // partial states, so it never stops aggregating. | ||
| for (expressions <- Seq("avg(big)", "avg(i), avg(big)")) { | ||
| assert(skipped(avgQuery(expressions)) == 0L, s"Unexpected skipping for $expressions") | ||
| } | ||
| // Whether or not a small decimal skips, the answer matches Spark's. | ||
| skipped(avgQuery("avg(small)")) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("first/last") { | ||
| withSQLConf( | ||
| SQLConf.COALESCE_PARTITIONS_ENABLED.key -> "true", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The gate checks for a
DOUBLEresult, not a non-decimal input. Spark rewritesavgover a decimal with precision 11 or lower intoavg(UnscaledValue(x)), which has aDOUBLEresult and is admitted, soavgoverDECIMAL(10, 2)does skip. Could this line, the matching one in the Eligibility paragraph below, and the.docinCometConfsayAVGexcept over decimals with precision above 11? Both lines here also run past the wrap the rest of the section uses.