Skip to content

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

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

feat(physical-plan): GroupColumn support for List / LargeList#23648
mzabaluev wants to merge 6 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.

@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

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