Skip to content

fix: release per-batch accumulator scratch after a clustered partial emit under memory pressure - #26782

Draft
viirya wants to merge 7 commits into
apache:mainfrom
viirya:accumulator-scratch-shrink
Draft

viirya wants to merge 7 commits into
apache:mainfrom
viirya:accumulator-scratch-shrink

Conversation

@viirya

@viirya viirya commented Oct 27, 2026

Copy link
Copy Markdown
Member

(Stacked on #26726. Until #26726 merges, this branch's diff also contains #26726's commits. Only the last commit belongs to this PR.)

Which issue does this PR close?

Rationale for this change

#26726 lets a fully clustered partial aggregate continue under memory pressure: it emits the completed groups, then releases the capacity that the group values and group_indices grew for the batch, so that reserving the table fits again. Accumulators can also keep per-batch scratch that size() counts, and GroupsAccumulator had no way to release it. Such a query still fails with ResourcesExhausted, although the table holds only the active group.

For example, with 8192-row batches and a 100 KiB pool:

  • APPROX_DISTINCT(k), APPROX_DISTINCT(v) GROUP BY k: each HllGroupsAccumulator keeps a 64 KiB hashes buffer, and the stream fails to reserve ~123 KB.
  • ANY_VALUE(v) GROUP BY k, or any aggregate without a native GroupsAccumulator: GroupsAccumulatorAdapter keeps its per-batch Scratch (groups_with_rows, offsets, batch_indices, ~160 KiB). It releases that only when every group is emitted, but the clustered emit keeps the active group. The stream fails to reserve ~283 KB.

What changes are included in this PR?

  • Add GroupsAccumulator::shrink_to_fit, which does nothing by default, documented like GroupValues::shrink_to_fit.
  • ClusteredAggregateTable::shrink_to_fit calls it for every accumulator, through HashAggregateAccumulator::shrink_to_fit.
  • HllGroupsAccumulator drops hashes and shrinks groups.
  • GroupsAccumulatorAdapter drops its Scratch and shrinks states, and updates allocation_bytes.

The other GroupsAccumulator implementations were checked and keep only per-group state (or no counted per-batch scratch), so they keep the default. ForeignGroupsAccumulator (FFI) also keeps the default, because forwarding the call would need a new function pointer in FFI_GroupsAccumulator. That can be a follow-up if a foreign accumulator needs it.

What is the testing strategy for this PR?

New test accumulator_scratch_under_memory_pressure in clustered_partial_stream.rs, next to completed_groups_under_memory_pressure and using the same pattern: a sorted Int64 key, batches whose boundaries share a key, a 100 KiB GreedyMemoryPool, and an exact comparison with an unbounded run. It covers APPROX_DISTINCT(k0), APPROX_DISTINCT(v) (HllGroupsAccumulator) and ANY_VALUE(v) (GroupsAccumulatorAdapter). Before this change, each case fails with ResourcesExhausted.

Are there any user-facing changes?

A new provided method, GroupsAccumulator::shrink_to_fit, with a no-op default. Existing implementations don't need to change.

This pull request and its description were written by Isaac.

viirya and others added 7 commits October 24, 2026 11:49
…acity

After the completed groups are emitted under memory pressure with full
group clustering, only one group remains, but the table keeps the
capacity it grew for the whole batch (the single-key hash table, the
clustered new_groups scratch and group_indices). Re-reserving that
retained size fails in a 100 KiB pool for 8192 distinct keys per batch.

This test fails on purpose; it demonstrates the issue and is not a fix.

Co-authored-by: Isaac <no-reply@databricks.com>
Run a sorted GROUP BY end to end with a pool that limits only the
clustered partial aggregate streams, since the downstream merge and
final aggregate cannot spill and a shared limit cannot single out the
partial stream. With 8192 distinct keys per batch the retained table
needs ~344 KB (one key) or ~131 KB (two keys) after the completed groups
are emitted, and a 100 KiB limit fails.

This test fails on purpose; it demonstrates the issue and is not a fix.

Co-authored-by: Isaac <no-reply@databricks.com>
… under full group clustering

On a refused reservation under full group clustering, the stream emitted
only the completed groups. The remaining table holds one group but keeps
the capacity it grew for the batch (the single-key hash table, the
clustered new_groups scratch and group_indices), so re-reserving it could
still fail with ResourcesExhausted.

Use take_state_batch as partial group clustering already does: emit every
state, including the active group, and reset the table. The final stage
merges the split active group, as it merges states of the same key from
different partitions.

Co-authored-by: Isaac <no-reply@databricks.com>
Under full group clustering, the partial stream now emits the state of
the one incomplete group on a refusal and finishes, instead of returning
the refusal.

Co-authored-by: Isaac <no-reply@databricks.com>
…rtial-oom

Co-authored-by: Isaac <no-reply@databricks.com>
…table

Resetting the table under full group clustering split the active group
into two state rows within one partition. The aggregate still declares
the group keys unique and ordered, so a sort on the keys and another
column above it could pass rows through out of order.

Emit only the completed groups again, as before, and then release the
capacity the table kept for them: a new GroupValues::shrink_to_fit,
implemented for GroupValuesPrimitive, GroupValuesClustered,
GroupValuesColumn and GroupValuesRows, and the group_indices scratch.

Merge the two unit tests into one that overlaps batch boundaries and
covers all four implementations, restore the refusal snapshot, and
drop the SQL test, which duplicated the unit test.

Co-authored-by: Isaac <no-reply@databricks.com>
…emit

Add GroupsAccumulator::shrink_to_fit, a no-op by default, and call it for every accumulator from ClusteredAggregateTable::shrink_to_fit. HllGroupsAccumulator drops its per-batch hashes, and GroupsAccumulatorAdapter drops its per-batch scratch, which it otherwise keeps while the active group remains.

Closes apache#26781.

Co-authored-by: Isaac <no-reply@databricks.com>
@github-actions github-actions Bot added logical-expr Logical plan and expressions functions Changes to functions implementation physical-plan Changes to the physical-plan crate labels Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

functions Changes to functions implementation logical-expr Logical plan and expressions physical-plan Changes to the physical-plan crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fully sorted partial aggregate keeps per-batch accumulator scratch reserved after emitting completed groups (APPROX_DISTINCT)

1 participant