@@ -218,10 +218,16 @@ struct GroupSlicer {
218218 auto oc = sliceInfos[index].getSliceFor (pos);
219219 uint64_t offset = oc.first ;
220220 auto count = oc.second ;
221- auto groupedElementsTable = originalTable.asArrowTable ()->Slice (offset, count);
222221 if (count == 0 ) {
223- return std::decay_t <A1>{{groupedElementsTable}, soa::SelectionVector{}};
222+ // Empty group: avoid slicing every column only to discard it. Cache one
223+ // empty (0-row) table per associated table and reuse it. This is the
224+ // common case for sparse grouping (e.g. collisions with no candidates).
225+ if (!emptyTables[index]) {
226+ emptyTables[index] = originalTable.asArrowTable ()->Slice (0 , 0 );
227+ }
228+ return std::decay_t <A1>{{emptyTables[index]}, soa::SelectionVector{}};
224229 }
230+ auto groupedElementsTable = originalTable.asArrowTable ()->Slice (offset, count);
225231
226232 // for each grouping element we need to slice the selection vector
227233 auto start_iterator = std::lower_bound (starts[index], selections[index]->end (), offset);
@@ -275,6 +281,9 @@ struct GroupSlicer {
275281 std::span<int64_t const > groupSelection;
276282 std::array<std::span<int64_t const > const *, sizeof ...(A)> selections;
277283 std::array<std::span<int64_t const >::iterator, sizeof ...(A)> starts;
284+ // Cached empty (0-row) table per associated table, lazily built and reused
285+ // for empty groups so we do not slice every column on each empty group.
286+ std::array<std::shared_ptr<arrow::Table>, sizeof ...(A)> emptyTables{};
278287
279288 std::array<SliceInfoPtr, sizeof ...(A)> sliceInfos;
280289 std::array<SliceInfoUnsortedPtr, sizeof ...(A)> sliceInfosUnsorted;
0 commit comments