[SPARK-59564][SQL] Combine adjacent aggregation across a GroupPartitionsExec - #58884
ulysses-you wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Thanks for the PR, @ulysses-you!
The rule now walks a chain of GroupPartitionsExec and local sorts to reach the partial aggregate, and withKeyPositionsFor moves the grouping's projected positions from the aggregate's key space into the child's. I traced that arithmetic and it is right: positions.map(newChildKp.expressions) reproduces the expression list the node reported before, and the projected key rows, dataTypes, isGrouped and isCollapsed come out the same, so outputPartitioning is unchanged and the combined aggregate's ClusteredDistribution stays satisfied. I also checked the direction your calibration would expect me to: the fold cannot regress, since it replaces two hash/sort passes with one over the same stream and the bail covers the shape where the kept sort would grow from the partial aggregate's output to the whole scan.
I built the branch and ran 21 probes of my own outside the suite's shapes - count-distinct, an aggregate over a storage-partitioned join grouping by either side's key, an alias on the grouping key, a grouping key that is not a plain attribute, partially-clustered distribution, a filter and a projection below, a reversed grouping order, both coalesce-ordering configs, WITH ROLLUP, a limit above, and AQE. In every one the rule-on answer matched the rule-off answer and ValidateRequirements.validate held. Nothing blocking from me.
Non-blocking
- 1. The bail's derived-ordering path is untested: the description singles this path out as the reason the bail is load-bearing "for a grouping that derives its ordering, not only for one whose source declares it", but the only bail test uses a source that declares its
ordering. I measured that the derived path is a distinct reachable shape and that nothing covers it. One extrawithSQLConfcloses it. inline:KeyGroupedPartitioningSuite.scala:6820
Minor
- 2.
withKeyPositionsForcites a producer whose expression list is discarded:KeyedPartitioning.projectbuilds the projected expressions, thenAliasAwareOutputExpression.projectKeyedPartitioningsreplaces them with its per-position cross-product. What actually makes the lookup work is that a partial aggregate'sresultExpressionsaregroupingAttributes ++ bufferAttributes, sohasAliasis false there and the alternatives are the child's own expressions. inline:GroupPartitionsExec.scala:562
| } | ||
| } | ||
|
|
||
| test("SPARK-59564: keep the pair where the source already orders the aggregate's input") { |
There was a problem hiding this comment.
Finding 1. This pins the declared-ordering arm of the bail. The description says the bail matters for more than that:
That last path is taken by more shapes than a source that declares its ordering: with
spark.sql.sources.v2.bucketing.partitionKeyOrdering.enabledthe ordering derived from the partition keys already satisfies the partial aggregate's, so it holds no sort of its own to give up. The bail is load-bearing for a grouping that derives its ordering, not only for one whose source declares it.
I measured that on 98632fd and it holds, on the fixture of the sort-aggregate test at :6717 rather than this one: the same table, the same three rows, the same query, only the conf differs.
V2_BUCKETING_PARTITION_KEY_ORDERING_ENABLED |
aggregates | sorts |
|---|---|---|
false (that test's arm) |
1, the fold | 1 |
true |
2, the bail | 1 |
So the conf alone flips that query from the fold to the bail, and nothing holds that arm. It also means the mutation check the description reports, that dropping the bail fails this test, only covers the declared-ordering source; the derived shape would stay silent under the same mutation.
An extra arm on the :6717 test is enough, since it needs neither a second table nor a declared ordering. This is what I ran, and it passes as written:
// The ordering derived from the partition keys satisfies the partial aggregate the way a
// declared one does, so the bail holds for it too.
withSQLConf(
SQLConf.V2_BUCKETING_PARTITION_KEY_ORDERING_ENABLED.key -> "true",
SQLConf.COMBINE_ADJACENT_AGGREGATION_ENABLED.key -> "true") {
val df = sql(query)
checkAnswer(df, expected)
val plan = df.queryExecution.executedPlan
val aggs = collect(plan) { case agg: BaseAggregateExec => agg }
assert(aggs.size == 2,
s"the derived ordering leaves the partial aggregate no sort to give up either:\n$plan")
}There was a problem hiding this comment.
Added the arm in aafc7cb, on the sort-aggregate test: with partitionKeyOrdering.enabled the
scan's derived ordering satisfies the partial aggregate, so it holds no sort of its own and the bail
keeps the pair. It asserts what the declared-ordering test does: 2 aggregates, one local sort reading
the grouping, the partial aggregate on the scan. Dropping the bail now fails that arm as well, so the
derived shape no longer goes unheld under that mutation.
| * `EnsureRequirements` computed `joinKeyPositions` against the child this node was planned for, | ||
| * whose partitioning is that child's projected down to the positions the operator above keeps, so | ||
| * the positions name a key space `newChild` need not share. The projected expressions are the | ||
| * planned child's own (`KeyedPartitioning.project` builds them that way), which makes moving them |
There was a problem hiding this comment.
Finding 2. The parenthetical names a producer whose expression list does not survive.
KeyedPartitioning.project does build positions.map(expressions) (partitioning.scala:786), but AliasAwareOutputExpression.projectKeyedPartitionings throws that away: it calls project only for the layout and then replaces the expressions with its per-position cross-product, projected.copy(expressions = projectedExprs) (AliasAwareOutputExpression.scala:149-158). The alternatives come from projectExpression, which under hasAlias yields the node's output attributes, not the child's expressions.
What actually makes the lookup work here is one step further out: a partial aggregate's resultExpressions are groupingAttributes ++ aggBufferAttributes (AggUtils.planAggregateWithoutDistinct), all plain attributes, so hasAlias is false on it and the no-alias branch keeps kp.expressions(i) as the child's own. That is also what makes indexOf safe rather than merely lucky: the no-alias branch filters per position by references.subsetOf(outputSet) and projectablePositions is ascending, so a found index is the position the aggregate projected it from and cannot be a coincidental hit at another position.
Worth saying, because the reachability of the whole method rests on it, and project reads as if it were the guarantee.
There was a problem hiding this comment.
Reworked in aafc7cb. The comment now credits
AliasAwareOutputExpression.projectKeyedPartitionings for keeping the projected expressions the
child's own, and names the condition it rests on: a partial aggregate has no aliases, its
resultExpressions being groupingAttributes ++ bufferAttributes, so each position's alternative
stays the child's expression rather than the one project built and the projection replaced.
98632fd to
aafc7cb
Compare
|
Thanks for the review @peter-toth . Both points are addressed in aafc7cb. |
|
thank you @peter-toth , also cc @cloud if you have other comments |
|
Thank you @ulysses-you and @peter-toth! |
dongjoon-hyun
left a comment
There was a problem hiding this comment.
+1, LGTM.
I traced the key-position translation, the attribute binding of the sort that crosses the aggregate, the enableSortedMerge carry-over and the marked-layout (mayContainUnknownPartitionKeys) path, and did not find a correctness hole. A few non-blocking comments, any of which can be a follow-up:
-
detachAggregateruns before the compatibility checks.group.withKeyPositionsFor(child)goes throughGroupPartitionsExec.apply->computeGrouping, which walks every split of the child and builds anInternalRowComparableWrapperper split. WhencombinedAggregate/isPartialAggthen rejects the pair (aPartialMerge+Finalobject-hash pair, alogicalLinkmismatch, ...), that work is thrown away. In theHashAggregateExecarm, evaluatingcombinedAggregatefirst would avoid it. -
withKeyPositionsForreads the livechild.outputPartitioningrather than the storedchildPartitioningthatjoinKeyPositionswas computed against. Where the two diverge,outputPartitioningdeliberately drops the keyed claim, but this path translates the positions against the live one and hands back a node as if the claim still held. No rule is known to perform such a rewrite today, so this is only about staying symmetric withcheckChildStillMatches. -
The sort handling keys off the presence of a local sort above the aggregate, not off its ordering. Today the only inserter of a local sort between the pair is
EnsureRequirementsforSortAggregateExec.requiredChildOrdering, andisPartialAggpins the two groupings equal, so the crossed sort always covers the one being dropped --PushDownLocalSortcannot get in between either, since it treats neither the aggregates norGroupPartitionsExecas order-preserving. That invariant lives in the comment rather than in the code; aSortOrder.orderingSatisfies(sort.sortOrder, aggregate.requiredChildOrdering.head)guard would keep it honest if a rule starts inserting sorts there. -
Two shapes the fold reaches that the suite does not pin: the composing branch of
joinKeyPositions.fold(plannedInNewChild)(_.map(plannedInNewChild))(the fold path always starts fromNone, the partial aggregate having already narrowed), and thehasUpperSort = falsebranch where the sort below the partial aggregate stays under the grouping.
|
Thanks @dongjoon-hyun. All four are in 4174278.
Both new tests discriminate by mutation: dropping the lower sort unconditionally fails the sort test, and dropping the child-partitioning turn-away fails the position test, re-parenting with Ran |
…onsExec
### What changes were proposed in this pull request?
`CombineAdjacentAggregation` now looks through the `GroupPartitionsExec` and the local sorts
`EnsureRequirements` may have put between the partial and the final aggregate, so the pair is
combined even when the final aggregate's distribution was satisfied without a shuffle.
The grouping is re-parented onto the aggregate's child with the key positions it projects moved into
that child's key space, which is what `GroupPartitionsExec.withKeyPositionsFor` answers. It was
planned against the aggregate, whose partitioning is the child's projected down to what the
aggregate's output keeps, so those positions name a key space the child being handed need not share.
A sort crossed above the aggregate orders the rows the combined aggregate reads, by the grouping the
two aggregates share, so the sort feeding the partial aggregate goes with it. Where the partial
aggregate holds no sort of its own, the pair is left alone instead, being then the only cardinality
reducer before that sort. With no sort crossed at all, the sort below the aggregate stays below it:
the aggregate reads what it read, and the ordering it claims is the one it had.
### Why are the changes needed?
The rule only matched a strictly adjacent pair, so it missed the shape where `EnsureRequirements`
satisfies the final aggregate's `ClusteredDistribution` with a `GroupPartitionsExec` rather than a
shuffle. Folding the pair there removes an aggregation pass, and where a sort fed the partial
aggregate, that sort as well.
### Does this PR introduce _any_ user-facing change?
Yes, the plan changes for an aggregate pair whose child needs a `GroupPartitionsExec`, e.g. for
`GROUP BY id, name` over a v2 table partitioned by `(id, name)` whose keys repeat across splits:
Before:
```
HashAggregate (Final)
+- GroupPartitions
+- HashAggregate (Partial)
+- BatchScan
```
After:
```
HashAggregate (Complete)
+- GroupPartitions
+- BatchScan
```
A grouping on part of the partition keys is covered too: the positions are moved onto the scan's key
space, which is a lookup, the projected expressions being the child's own. Where a sort sits between
the pair it goes with the partial aggregate, so two sorts become one. Where the partial aggregate
holds no sort of its own and a sort above orders the combined aggregate's rows, the pair is left
alone and the plan is unchanged.
That last path is taken by more shapes than a source that declares its ordering: with
`spark.sql.sources.v2.bucketing.partitionKeyOrdering.enabled` the ordering derived from the partition
keys already satisfies the partial aggregate's, so it holds no sort of its own to give up. The bail is
load-bearing for a grouping that derives its ordering, not only for one whose source declares it.
Results are unchanged.
### How was this patch tested?
New tests in `KeyGroupedPartitioningSuite`: the pair combined across the grouping, including the
object-hash pair and the AQE path; the sort pair with the sort in between; a grouping on part of the
partition keys combined with its positions moved; a sort pair whose grouping covers part of the
partition keys; a grouping the scan reports narrowed itself; the pair kept where the source already
orders the aggregate's input; and a comparison against the plan `bypassPartialAggregation` builds for
the same query, whose grouping projects what the fold's does.
Each asserts the aggregates left and their mode, the grouping's positions where they matter, and
`ValidateRequirements.validate(plan)`, and compares the answer against the rule-off run. The two sort
tests' rule-off arms pin the two sorts the fold takes down to one.
Verified the tests discriminate by mutation: keeping the positions unmoved fails both narrowed-grouping
tests with three rows instead of two, and dropping the sort bail fails the ordered-source test with
one aggregate instead of two.
Ran `sql/Test/compile`, `KeyGroupedPartitioningSuite` (191), and `AdaptivePartialAggregationSuite`,
`CombineAdjacentAggregationSuite`, `DataFrameAggregateSuite`, `EnsureRequirementsSuite`,
`GroupPartitionsExecSuite`, `PushDownLocalSortSuite`, `RemoveRedundantSortsSuite`,
`RemoveRedundantWindowGroupLimitsSuite`, `ReplaceHashWithSortAggSuite` and `SQLMetricsSuite`
(603 in total, no failures).
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (deepseek-flash)
Assisted-by: Claude Code (deepseek-flash)
…he derived-ordering bail `withKeyPositionsFor` handed the node to a new child with `copy`, which carries `grouping`, the partitioning it reports and the child partitioning it was decided over. A node reports that partitioning only while its child still reports the one it was planned over, and throws at execution otherwise, so the fold failed on a narrowed grouping instead of folding. Rebuild through `GroupPartitionsExec.apply` with the translated positions, so all three are decided over the new child; an aligned node is turned away, its slot order being the parent distribution's rather than a function of the child. `withKeyPositionsFor`'s doc then credits `AliasAwareOutputExpression.projectKeyedPartitionings` for keeping the projected expressions the child's own: `KeyedPartitioning.project` builds them, but the projection replaces them, and only its no-alias arm keeps the child's expression. A partial aggregate takes that arm, its `resultExpressions` being `groupingAttributes ++ bufferAttributes`. The sort-aggregate test gains an arm for the ordering derived from the partition keys, which leaves the partial aggregate no sort of its own, and asserts what its declared-ordering sibling does: one local sort, reading the grouping, with the partial aggregate on the scan. Assisted-by: deepseek-flash v4.1
… hold the child and sort invariants `detachAggregate` takes the aggregate the chain is folded into and checks the leaf against it before the chain is rebuilt: rebuilding re-decides the grouping over the new child, which walks that child's partitions, so only a pair that can be folded is worth it. That is the only compatibility check, so the arms ask the leaf only for its mode. `withKeyPositionsFor` turns away a child that no longer reports the partitioning this node was decided for, and reads the projected expressions from the stored `childPartitioning`. The positions name keys in that space, so moving them against another one re-targets the grouping. The sort below the partial aggregate goes only when it satisfies that aggregate's required child ordering, so an incidental sort no longer leaves with it. The two shapes no test pinned are covered: the positions moving into a child that reports its keys in another order, in `GroupPartitionsExecSuite`, where the composition can be built directly, and the sort staying under the regrouping where no sort is crossed. Assisted-by: deepseek-flash v4.1
… the defaults `partitionKeyOrdering.enabled` and `preserveKeyOrderingOnCoalesce.enabled` default to true since SPARK-59396 (5.0.0). The ordering a scan derives from its partition keys is the grouping of these tests, and a regrouping reports the key ordering it keeps, so under the defaults neither aggregate holds a sort of its own and none lands between the pair: the pair folds with no sort at all, the regrouping reading the scan. The sort tests assert that instead of the sort-driven shapes they were written for, one of them with the sort the partial aggregate holds staying under the regrouping. Two shapes need a sort between the pair, which needs the regrouping not to report the ordering it kept. The bail test and the bail arm of the sort test take `preserveKeyOrderingOnCoalesce.enabled=false` for it, and the arm covering the sort the fold takes with the partial aggregate takes both orderings off, which is the configuration that shape needs. `ReplaceHashWithSortAgg` plans the combined aggregate sort-based once the regrouping reports an ordering that satisfies the grouping, so the `collect_set` arm asserts the complete mode and the answer rather than the aggregate kind. The `isPartialAgg` doc comment is rewrapped to fit the 100-character limit. Assisted-by: deepseek-flash v4.1
4174278 to
7ff8143
Compare
|
Rebased onto master, which now carries SPARK-59396: Two shapes need a sort between the pair, which needs the regrouping not to report its ordering, so the bail test and the bail arm set
Ran |
…onsExec
### What changes were proposed in this pull request?
`CombineAdjacentAggregation` now looks through the `GroupPartitionsExec` and the local sorts
`EnsureRequirements` may have put between the partial and the final aggregate, so the pair is
combined even when the final aggregate's distribution was satisfied without a shuffle.
The grouping is rebuilt over the aggregate's child with the key positions it projects moved into
that child's key space, which is what `GroupPartitionsExec.withKeyPositionsFor` answers. It was
planned against the aggregate, whose partitioning is the child's projected down to what the
aggregate's output keeps, so those positions name a key space the child being handed need not share.
A node is decided for one child -- its grouping, the partitioning it reports and the child
partitioning it was decided over -- so it comes back through `GroupPartitionsExec.apply` rather than
through `copy`, which would carry that decision over. An aligned node is turned away. So is a node
whose child no longer reports the partitioning it was decided for: the positions name keys in that
space, and the node has already given up its claim for it.
A sort crossed above the aggregate orders the rows the combined aggregate reads, by the grouping the
two aggregates share, so the sort feeding the partial aggregate goes with it, when it is the sort
that aggregate was planned against, the ordering its required child ordering names. Where the
partial aggregate holds no sort of its own, the pair is left alone instead, being then the only
cardinality reducer before that sort. With no sort crossed at all, the sort below the aggregate
stays below it: the aggregate reads what it read, and the ordering it claims is the one it had.
### Why are the changes needed?
The rule only matched a strictly adjacent pair, so it missed the shape where `EnsureRequirements`
satisfies the final aggregate's `ClusteredDistribution` with a `GroupPartitionsExec` rather than a
shuffle. Folding the pair there removes an aggregation pass, and where a sort fed the partial
aggregate, that sort as well.
### Does this PR introduce _any_ user-facing change?
Yes, the plan changes for an aggregate pair whose child needs a `GroupPartitionsExec`, e.g. for
`GROUP BY id, name` over a v2 table partitioned by `(id, name)` whose keys repeat across splits:
Before:
```
HashAggregate (Final)
+- GroupPartitions
+- HashAggregate (Partial)
+- BatchScan
```
After:
```
HashAggregate (Complete)
+- GroupPartitions
+- BatchScan
```
A grouping on part of the partition keys is covered too: the positions are moved onto the scan's key
space, which is a lookup, the projected expressions being the child's own. Where a sort sits between
the pair it goes with the partial aggregate, so two sorts become one. Where the partial aggregate
holds no sort of its own and a sort above orders the combined aggregate's rows, the pair is left
alone and the plan is unchanged.
That last path is taken by more shapes than a source that declares its ordering: with
`spark.sql.sources.v2.bucketing.partitionKeyOrdering.enabled` the ordering derived from the
partition keys already satisfies the partial aggregate's, so it holds no sort of its own to give up.
The bail is load-bearing for a grouping that derives its ordering, not only for one whose source
declares it, and it needs the regrouping not to report that ordering: a sort lands between the pair
only then, which the defaults do not leave.
Results are unchanged.
### How was this patch tested?
New tests in `KeyGroupedPartitioningSuite`: the pair combined across the grouping, including the
object-hash pair and the AQE path; the sort pair with the sort in between; a grouping on part of the
partition keys combined with its positions moved; a sort pair whose grouping covers part of the
partition keys; a grouping the scan reports narrowed itself; the pair kept where the source already
orders the aggregate's input, and where the ordering is derived from the partition keys; and a
comparison against the plan `bypassPartialAggregation` builds for the same query, whose grouping
projects what the fold's does, the sort the partial aggregate holds staying under the regrouping
where no sort is crossed, and the pair folded over `collect_set`. The positions moving into a child
that reports the same keys in another order are covered in `GroupPartitionsExecSuite`, where that
composition can be built directly.
Each asserts the aggregates left and their mode, the grouping's positions where they matter, and
`ValidateRequirements.validate(plan)`, and compares the answer against the rule-off run. The two
sort tests' rule-off arms pin the sorts the fold takes down to one. Each test asserts the shape the
defaults leave, where the ordering the source derives and the key ordering the regrouping reports
satisfy both aggregates and the pair folds with no sort at all; the shapes that need a sort between
the pair, which needs the regrouping not to report its ordering, are the ones that set
`preserveKeyOrderingOnCoalesce.enabled=false`, or both orderings off for the sort the fold takes
with the partial aggregate.
Verified the tests discriminate by mutation: keeping the positions unmoved fails both
narrowed-grouping tests with three rows instead of two, and dropping the sort bail fails the
ordered-source test with one aggregate instead of two. Dropping the lower sort unconditionally fails
the new sort test, and dropping the child-partitioning turn-away fails the new position test,
re-parenting onto a key space the node was not decided for.
Ran `sql/Test/compile`, `KeyGroupedPartitioningSuite` and `GroupPartitionsExecSuite` (225), and
`AdaptivePartialAggregationSuite`, `CombineAdjacentAggregationSuite`, `DataFrameAggregateSuite`,
`EnsureRequirementsSuite`, `PushDownLocalSortSuite`, `RemoveRedundantSortsSuite`,
`RemoveRedundantWindowGroupLimitsSuite`, `ReplaceHashWithSortAggSuite` and `SQLMetricsSuite` (393),
618 in total, no failures.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (deepseek-flash v4.1)
Closes #58884 from ulysses-you/SPARK-59564.
Authored-by: Xiduo You <ulyssesyou18@gmail.com>
Signed-off-by: Xiduo You <ulyssesyou@apache.org>
(cherry picked from commit f0be43c)
Signed-off-by: Xiduo You <ulyssesyou@apache.org>
|
thank you all! |
What changes were proposed in this pull request?
CombineAdjacentAggregationnow looks through theGroupPartitionsExecand the local sortsEnsureRequirementsmay have put between the partial and the final aggregate, so the pair iscombined even when the final aggregate's distribution was satisfied without a shuffle.
The grouping is rebuilt over the aggregate's child with the key positions it projects moved into
that child's key space, which is what
GroupPartitionsExec.withKeyPositionsForanswers. It wasplanned against the aggregate, whose partitioning is the child's projected down to what the
aggregate's output keeps, so those positions name a key space the child being handed need not share.
A node is decided for one child -- its grouping, the partitioning it reports and the child
partitioning it was decided over -- so it comes back through
GroupPartitionsExec.applyrather thanthrough
copy, which would carry that decision over. An aligned node is turned away. So is a nodewhose child no longer reports the partitioning it was decided for: the positions name keys in that
space, and the node has already given up its claim for it.
A sort crossed above the aggregate orders the rows the combined aggregate reads, by the grouping the
two aggregates share, so the sort feeding the partial aggregate goes with it, when it is the sort
that aggregate was planned against, the ordering its required child ordering names. Where the
partial aggregate holds no sort of its own, the pair is left alone instead, being then the only
cardinality reducer before that sort. With no sort crossed at all, the sort below the aggregate
stays below it: the aggregate reads what it read, and the ordering it claims is the one it had.
Why are the changes needed?
The rule only matched a strictly adjacent pair, so it missed the shape where
EnsureRequirementssatisfies the final aggregate's
ClusteredDistributionwith aGroupPartitionsExecrather than ashuffle. Folding the pair there removes an aggregation pass, and where a sort fed the partial
aggregate, that sort as well.
Does this PR introduce any user-facing change?
Yes, the plan changes for an aggregate pair whose child needs a
GroupPartitionsExec, e.g. forGROUP BY id, nameover a v2 table partitioned by(id, name)whose keys repeat across splits:Before:
After:
A grouping on part of the partition keys is covered too: the positions are moved onto the scan's key
space, which is a lookup, the projected expressions being the child's own. Where a sort sits between
the pair it goes with the partial aggregate, so two sorts become one. Where the partial aggregate
holds no sort of its own and a sort above orders the combined aggregate's rows, the pair is left
alone and the plan is unchanged.
That last path is taken by more shapes than a source that declares its ordering: with
spark.sql.sources.v2.bucketing.partitionKeyOrdering.enabledthe ordering derived from thepartition keys already satisfies the partial aggregate's, so it holds no sort of its own to give up.
The bail is load-bearing for a grouping that derives its ordering, not only for one whose source
declares it, and it needs the regrouping not to report that ordering: a sort lands between the pair
only then, which the defaults do not leave.
Results are unchanged.
How was this patch tested?
New tests in
KeyGroupedPartitioningSuite: the pair combined across the grouping, including theobject-hash pair and the AQE path; the sort pair with the sort in between; a grouping on part of the
partition keys combined with its positions moved; a sort pair whose grouping covers part of the
partition keys; a grouping the scan reports narrowed itself; the pair kept where the source already
orders the aggregate's input, and where the ordering is derived from the partition keys; and a
comparison against the plan
bypassPartialAggregationbuilds for the same query, whose groupingprojects what the fold's does, the sort the partial aggregate holds staying under the regrouping
where no sort is crossed, and the pair folded over
collect_set. The positions moving into a childthat reports the same keys in another order are covered in
GroupPartitionsExecSuite, where thatcomposition can be built directly.
Each asserts the aggregates left and their mode, the grouping's positions where they matter, and
ValidateRequirements.validate(plan), and compares the answer against the rule-off run. The twosort tests' rule-off arms pin the sorts the fold takes down to one. Each test asserts the shape the
defaults leave, where the ordering the source derives and the key ordering the regrouping reports
satisfy both aggregates and the pair folds with no sort at all; the shapes that need a sort between
the pair, which needs the regrouping not to report its ordering, are the ones that set
preserveKeyOrderingOnCoalesce.enabled=false, or both orderings off for the sort the fold takeswith the partial aggregate.
Verified the tests discriminate by mutation: keeping the positions unmoved fails both
narrowed-grouping tests with three rows instead of two, and dropping the sort bail fails the
ordered-source test with one aggregate instead of two. Dropping the lower sort unconditionally fails
the new sort test, and dropping the child-partitioning turn-away fails the new position test,
re-parenting onto a key space the node was not decided for.
Ran
sql/Test/compile,KeyGroupedPartitioningSuiteandGroupPartitionsExecSuite(225), andAdaptivePartialAggregationSuite,CombineAdjacentAggregationSuite,DataFrameAggregateSuite,EnsureRequirementsSuite,PushDownLocalSortSuite,RemoveRedundantSortsSuite,RemoveRedundantWindowGroupLimitsSuite,ReplaceHashWithSortAggSuiteandSQLMetricsSuite(393),618 in total, no failures.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (deepseek-flash v4.1)