fix: omit ordering fields from the state of order-insensitive aggregates - #25402
neilconway merged 4 commits into
Conversation
`min(v ORDER BY k)` and `max(v ORDER BY k)` in a grouped query failed with "number of columns(2) must match number of fields(3) in schema" whenever the aggregation ran in two phases, as it does with the default target_partitions. AggregateFunctionExpr::order_bys() already returns no expressions for an order-insensitive aggregate, so its ORDER BY columns are never fed to the accumulator. state_fields() still passed ordering_fields, though, and the default AggregateUDFImpl::state_fields, which Min and Max use, appends them. The accumulators only emit the value, so the partial state schema had fields with no columns behind them. Pass no ordering fields for order-insensitive aggregates, mirroring order_bys(). Part of apache#25401
neilconway
left a comment
There was a problem hiding this comment.
Thanks @hassaanch23 ! This is a good analysis of the root cause of the bug. The approach we're taking in this PR is okay, but it leaves the state of the agg a little inconsistent (ordering_fields is defined but most code should probably check if the agg is order-insensitive before using it).
I wonder if it would be cleaner to clear order_bys in AggregateExprBuilder::build() for order-insensitive aggregates. That way ordering_fields is empty, so we avoid the inconsistent state in the first place. What do you think?
…regate-ordering-state
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #25402 +/- ##
=======================================
Coverage 82.32% 82.32%
=======================================
Files 1137 1137
Lines 431824 431883 +59
Branches 431824 431883 +59
=======================================
+ Hits 355500 355563 +63
+ Misses 54838 54824 -14
- Partials 21486 21496 +10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Instead of passing empty ordering fields from state_fields(), drop the ORDER BY in AggregateExprBuilder::build() when the function is order-insensitive. The ordering fields are then empty from the start, so the aggregate carries no ordering state that later code has to ignore. Also cover the ungrouped case. It fails the same way once the aggregation runs in Partial and Final modes; the filter makes the single-partition `sales_global` input multi-partition so the test reaches that path.
|
@neilconway Thanks, that's cleaner. I pushed cc17378. Before switching, I checked two things. None of the built-in order-insensitive aggregates ( I also merged |
neilconway
left a comment
There was a problem hiding this comment.
Awesome, @hassaanch23 ! Nice work, I like this approach.
This is optional, but the SLTs are a bit fragile because they depend on the planner choosing a two-phase plan. We could also add a unit test that builds an order-insensitive UDAF with .order_by(), and then assert that its order_bys are empty. I got Claude to whip up something quickly: https://gist.github.com/neilconway/551457d97cea924b879be0dbc256deb7
AggregateExprBuilder::build() now guarantees that an order-insensitive aggregate has no order_bys, so order_bys() can return them directly and with_new_expressions no longer needs to special-case order-insensitive aggregates. Add unit tests that build an aggregate using the default state_fields with .order_by() and check its order_bys, its ORDER BY expressions, its state fields and what reaches the accumulator, for an order-insensitive and an order-sensitive function. Unlike the sqllogictests, they don't depend on the planner choosing a two-phase plan. Co-authored-by: Neil Conway <neil.conway@gmail.com>
|
@neilconway Thanks! Pushed 2a95a74. It has your unit tests unchanged, with you credited as co-author, and both simplifications. With the builder now clearing |
|
@hassaanch23 Excellent! Thank you for the great PR and the quick turnaround time. |
Which issue does this PR close?
min/maxschema mismatch. Theavg/bit_*/stddev/var_*panics have a different cause and are fixed in fix: mark avg, bit_and/or/xor, stddev and variance as order-insensitive #25403; the two PRs are independent.Rationale for this change
An order-insensitive aggregate called with an
ORDER BYfails whenever the aggregation runs in two phases (Partial→Final/FinalPartitioned), grouped or not:AggregateExprBuilder::build()derived the aggregate'sordering_fieldsfrom itsORDER BYeven when the function is order-insensitive.AggregateFunctionExpr::order_bys()hides those expressions from the accumulator, butstate_fields()still passedordering_fields.MinandMaxuse the defaultAggregateUDFImpl::state_fields, which appends them, while their accumulators only emit the value. The partial state schema therefore declared fields that no column filled.sum,count,bool_andandbool_oroverridestate_fieldswithout ordering fields, so they already worked.What changes are included in this PR?
AggregateExprBuilder::build()now dropsorder_byswhen the function'sorder_sensitivity()is insensitive.ordering_fieldsis then empty from the start, so the aggregate carries no ordering state that later code has to ignore. Order-sensitive aggregates are unchanged.None of the built-in order-insensitive aggregates reads
order_bysfromAccumulatorArgs, and plans display aggregates byname(), so EXPLAIN output does not change.What is the testing strategy for this PR?
Two queries in
group_by.slt, in the section that setstarget_partitions = 8, each selectingMIN,MAXandSUMwithORDER BY ts DESCoversales_global, withSUMas a control:country. The plan isPartial→FinalPartitioned. Onmainit fails withnumber of columns(4) must match number of fields(6).WHERE amount > 0so the input is multi-partition and the plan isPartial→Final. Onmainit fails withnumber of columns(3) must match number of fields(5). Without the filter, the single-partition input runs inSinglemode and never builds a partial state.Both queries pass with this change. The other
.sltfiles covering aggregates still pass (group_by,aggregate,array_agg,first_last_*,distinct_on,agg_func_substitute,window,order).Are there any user-facing changes?
Queries that failed now return results. No API changes.
Notes for reviewers
The state field names for ordering fields are being reworked in #25196. This PR only changes whether order-insensitive aggregates have ordering fields at all, so the two shouldn't interact beyond a possible textual rebase.