fix(physical-optimizer): decline eager aggregation for grouping-set aggregates - #190
Open
claudespice wants to merge 1 commit into
Open
Conversation
…ggregates
`EagerAggregation` rebuilds the top grouping with
`PhysicalGroupBy::new_single`, which cannot express `null_expr`, `groups`,
or `has_grouping_set`. Applied to a `GROUPING SETS`/`ROLLUP`/`CUBE`
aggregate it therefore returns a plain grouping, dropping both the
grouping-set rows and the `__grouping_id` column that `group_fields`
appends for them.
The rule opts out of `schema_check` (COUNT->SUM widens non-null to
nullable), so nothing catches the narrowed output schema. A parent
projection or window keeps binding aggregate outputs at their original
indices and trips the `col.name() == matching_name` assertion in
`ProjectionMapping`:
Internal error: Assertion failed: col.name() == matching_name:
Input field name sum(store_sales.ss_ext_sales_price) does not match
with the projection expression sum(store_sales.ss_net_profit).
Decline the rewrite when the partial aggregate's grouping reports
`has_grouping_set()`. The guard is on the *partial* grouping because
`as_final` always clears the flag, folding `__grouping_id` into its
expression list, so the final aggregate's grouping cannot report it.
Adds two regression tests over the existing beneficial-join fixture,
grouped by `ROLLUP(d_name)` so the grouping set is the only difference:
one asserts no pre-aggregation is pushed, one asserts the aggregate
output schema (including `__grouping_id`) is unchanged. Both fail without
the guard. The plain-grouping fixture is reused via a new
`agg_over_join_grouped` helper, so the accept path stays covered.
Refs spiceai/spiceai#11827
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EagerAggregationproduces a physically invalid plan for aGROUPING SETS/ROLLUP/CUBEaggregate over a join. Spice hits it on TPC-DS q36 (which groups byROLLUP(i_category, i_class)and reads the aggregate outputs from a window function), because Spice enables this rule by default:Root cause
The rewrite rebuilds the top grouping from the flat expression list only:
PhysicalGroupBy::new_singlehard-codesnull_expr: vec![], a single all-falsegroupsrow, andhas_grouping_set: false. A grouping-set aggregate therefore comes back as a plainGROUP BY, dropping both the extra grouping-set rows and the__grouping_idcolumn thatgroup_fieldsappends wheneverhas_grouping_setis set. The AVG output projection has the same gap — it iteratesgroup.expr(), which never includes__grouping_id.Nothing downstream catches the narrowed schema, because the rule opts out of
schema_check(COUNT decomposes to SUM, widening non-null to nullable). The parent projection/window keeps binding aggregate outputs at their original indices, so the column at the__grouping_idslot is read as the first aggregate and the assertion inProjectionMappingfires.Confirmed by neutering the new guard — the aggregate output schema loses a column:
Fix
Decline the rewrite when the partial aggregate's grouping reports
has_grouping_set().The guard is on the partial grouping deliberately:
as_final()always returnshas_grouping_set: false(it folds__grouping_idinto its expression list instead), sotop_final.group_expr()can never report a grouping set and a guard there would miss the bug entirely.This is the minimum safe fix. Supporting grouping sets properly means preserving the partial grouping's full shape — remapping
null_expralongsideexprinto the rebuilt join's schema and keeping__grouping_idat its original output index, sinceGROUPING(...)results are bound positionally by parent projections and windowPARTITION BY/ORDER BY. That is recorded in the module's "Deferred / not yet supported" list rather than attempted here.Testing
cargo test -p datafusion-physical-optimizer --lib— 58 passed, 0 failed (was 56 before this change).Two new regression tests reuse the existing beneficial-join fixture but group by
ROLLUP(d_name), so the grouping set is the only difference fromfires_on_beneficial_joinand the cost gate reaches the same verdict:declines_grouping_set_aggregate— no pre-aggregation is pushed below the join.grouping_set_aggregate_preserves_output_schema— the aggregate output schema,__grouping_idincluded, is unchanged by the rule.Both were verified to fail with the guard neutered to
if false(56 passed; 2 failed) and pass with it.The plain-grouping fixture now routes through a new
agg_over_join_groupedhelper that takes thePhysicalGroupBy, so all existing accept-path tests still exercise the unchanged behaviour (no snapshot updates were needed).cargo clippy -p datafusion-physical-optimizer --lib --tests -- -Dwarningsandcargo fmtare clean for the touched file.Notes
GROUP BY, and none at all upstream, whereenable_eager_aggregationdefaults tofalse.