kosiew commented on code in PR #23695:
URL: https://github.com/apache/datafusion/pull/23695#discussion_r3663622885


##########
datafusion/physical-plan/src/aggregates/mod.rs:
##########
@@ -2549,6 +2571,119 @@ impl AggregateExec {
     }
 }
 
+/// The type used internally to intern and emit a group-by key of the given
+/// input type.
+///
+/// String and binary group keys are accumulated with 64-bit offsets
+/// (`LargeUtf8` / `LargeBinary`) so that the single array holding all
+/// distinct group keys is not limited to `i32::MAX` total bytes. This is an
+/// implementation detail of the aggregation streams: output batches are
+/// narrowed back to the declared output types after being sliced to
+/// `batch_size` rows, where the narrow representation always fits.
+pub(crate) fn internal_group_key_type(input_type: &DataType) -> DataType {
+    match input_type {
+        DataType::Utf8 => DataType::LargeUtf8,
+        DataType::Binary => DataType::LargeBinary,
+        other => other.clone(),
+    }
+}
+
+/// Returns `schema` with any string/binary fields among the leading
+/// `num_group_columns` group key fields widened to the internal group key
+/// representation (see [`internal_group_key_type`]).
+///
+/// Returns the original schema when nothing needs widening.
+pub(crate) fn widen_group_key_schema(
+    schema: &SchemaRef,
+    num_group_columns: usize,
+) -> SchemaRef {
+    let mut changed = false;
+    let fields = schema
+        .fields()
+        .iter()
+        .enumerate()
+        .map(|(idx, field)| {
+            if idx < num_group_columns {
+                let wide = internal_group_key_type(field.data_type());
+                if wide != *field.data_type() {
+                    changed = true;
+                    return 
Arc::new(field.as_ref().clone().with_data_type(wide));
+                }
+            }
+            Arc::clone(field)
+        })
+        .collect::<Vec<_>>();
+
+    if changed {
+        Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone()))
+    } else {
+        Arc::clone(schema)
+    }
+}
+
+/// Widens string/binary group key arrays to the internal representation
+/// interned by `group_values` (see `PhysicalGroupBy::group_schema`).
+pub(crate) fn widen_group_key_arrays(arrays: Vec<ArrayRef>) -> 
Result<Vec<ArrayRef>> {

Review Comment:
   One performance thought. `ByteGroupValueBuilder` now accepts both narrow and 
large input arrays, so I wonder if the per input batch `arrow::compute::cast` 
in `widen_group_key_arrays` is still necessary.
   
   If the hashing, ordering, spilling and merge paths do not actually require 
the grouping arrays to be widened ahead of time, it may be possible to remove 
this cast entirely. That would avoid some extra offset and value buffer work on 
the hot aggregation path.



##########
datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs:
##########
@@ -253,7 +274,13 @@ impl<AggrMode> AggregateHashTable<AggrMode> {
                 }
             };
 
-        let batch = output.next_batch(batch_size);
+        // Narrow internally-widened group key columns back to the output
+        // schema types. Each slice holds at most `batch_size` rows, so unlike
+        // the full materialized batch, the narrow representation fits.
+        let batch = output
+            .next_batch(batch_size)

Review Comment:
   I think there is still one important edge case here. The output is split by 
row count before the `LargeUtf8` / `LargeBinary` group keys are narrowed back 
to the declared `Utf8` / `Binary` schema.
   
   The Arrow limit for narrow string and binary arrays is based on the total 
value buffer size, not the number of rows. That means a `batch_size` slice can 
still exceed `i32::MAX` bytes if it contains many large distinct keys 
accumulated across input batches.
   
   In that case this change moves the overflow from group value accumulation to 
the later `narrow_group_key_columns` cast, so the query can still fail.
   
   I noticed the same assumption in 
`datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs:827` and 
`datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs:432`.
   
   Could we instead split emitted output based on the narrow offset byte limit, 
or otherwise guarantee that each emitted slice is below that limit? It would 
also be great to add a regression that exercises this boundary.



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

To unsubscribe, e-mail: [email protected]

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


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

Reply via email to