Skip to content

feat(physical-plan): GroupColumn support for List / LargeList - #23648

Open
mzabaluev wants to merge 9 commits into
apache:mainfrom
mzabaluev:nested-group-column-for-lists
Open

feat(physical-plan): GroupColumn support for List / LargeList#23648
mzabaluev wants to merge 9 commits into
apache:mainfrom
mzabaluev:nested-group-column-for-lists

Conversation

@mzabaluev

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

This adds column-native GroupColumn implementations for List<T> and LargeList<T>, so a GROUP BY containing these (along with other supported data types) no longer falls back from GroupValuesColumn to GroupValuesRows.

What changes are included in this PR?

Cherry-picked @zhuqi-lucas's changes in #22706 that pertained to List / LargeList, and made some optimizations.

Are these changes tested?

Cherry-picked the unit tests for the list module from #22706.
Added supported/unsupported cases for List in the group_column_supported_type_matches_make_group_column test.

Benchmarked in a proprietary application using several HashAggregate operations on 20 columns including two string lists, to about 8% cumulative improvement.

Are there any user-facing changes?

No.

mzabaluev and others added 3 commits July 16, 2026 14:32
This is a minimal cherry-pick of the changes in
apache#22706, adding GroupColumn
support specifically for List and LargeList data types.

Co-Authored-By: Qi Zhu <821684824@qq.com>
Saves allocation of a new array.
Comment on lines +109 to +113
for j in 0..lhs_len {
if !self.child.equal_to(lhs_start + j, &rhs_sublist, j) {
return false;
}
}

This comment was marked as resolved.

Comment on lines +128 to +130
for j in 0..n {
self.child.append_val(&sublist, j)?;
}

This comment was marked as resolved.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.21429% with 11 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@95de385). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...src/aggregates/group_values/multi_group_by/list.rs 98.10% 4 Missing and 7 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #23648   +/-   ##
=======================================
  Coverage        ?   80.67%           
=======================================
  Files           ?     1087           
  Lines           ?   366711           
  Branches        ?   366711           
=======================================
  Hits            ?   295859           
  Misses          ?    53250           
  Partials        ?    17602           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +1075 to +1081
DataType::List(child_field) => {
let child = make_group_column(child_field.as_ref())?;
v.push(Box::new(list::ListGroupValueBuilder::<i32>::new(
Arc::clone(child_field),
child,
)));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about using ListGroupValueBuilder::<i32>. What happens when the concatenated length of all the children exceeds i32::MAX? I think ListGroupValueBuilder should drop the O: OffsetSizeTrait generic and always use usize to store its offsets.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it, the build/take_n of this builder must produce a ListArray, with i32 offsets. So an overflow would result in an error.

@mzabaluev
mzabaluev requested a review from KonaeAkira July 17, 2026 00:03
Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/list.rs Outdated
Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/list.rs Outdated
Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/list.rs Outdated
Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/list.rs Outdated
Co-authored-by: Trương Hoàng Long <longtruong2411@gmail.com>
@mzabaluev-flarion
mzabaluev-flarion force-pushed the nested-group-column-for-lists branch from 7ee5048 to 553bd2e Compare July 17, 2026 09:52
Comment on lines -221 to -234
// First-n offsets: 0, off[1], ..., off[n].
let first_n_offsets: Vec<O> = self.offsets[..=n].to_vec();

// Remaining offsets shifted so that what was offsets[n] becomes 0.
// Overwrite the array in place.
// SAFETY: the write range is at most as large as offsets.len().
// Values in the possible overlap are read before being overwritten.
unsafe {
let dst = self.offsets.as_mut_ptr();
for (i, &off) in self.offsets[n..].iter().enumerate() {
*dst.add(i) = off - cut_offset;
}
}
self.offsets.truncate(self.offsets.len() - n);

@mzabaluev mzabaluev Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KonaeAkira if we're going into micro-benchmarking territory, the previous code might be better in smaller take case when the remaining vector is drained in place, because it's subtract-while-copying vs. copy, then subtract in place. But this highly depends on vectorization, and the other case benefits from a smaller alloc-and-copy. Maybe there needs to be another split helper with a map closure to address both cases.

@KonaeAkira KonaeAkira Jul 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing that just occurred to me: split_vec_min_alloc(&mut self.offsets, n) returns a vector with exact capacity in case n * 2 <= self.offsets.len():

pub fn split_vec_min_alloc<T>(vec: &mut Vec<T>, n: usize) -> Vec<T> {
if n * 2 <= vec.len() {
vec.drain(0..n).collect()
} else {
let remaining = vec.split_off(n);
std::mem::replace(vec, remaining)
}
}

in which case first_n_offsets.push(cut_offset); will always reallocate, which is bad.

Maybe a separate PR can address this. This affects bytes.rs as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Followed up with #23730.

@KonaeAkira KonaeAkira left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Post-merge adaptations: Float16 nested type now has `GroupColumn`
support. Use RunEndEncoded as the unsupported leaf type case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-plan Changes to the physical-plan crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants