fix(plan): support nested correlated scalar aggregates - #26480
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
XuPeng-SH
left a comment
There was a problem hiding this comment.
Blocking correctness issue in pkg/sql/plan/flatten_subquery.go: the new deepScalarAggregate guard assumes every aggregate except count/starcount is NULL-on-empty. That is not true for approx_count and approx_count_distinct (their executor has emptyNull=false and flushes an empty sketch as 0). pullupThroughAgg turns the correlated inner expression into a GROUP BY key, so an outer key with no matching inner rows loses the aggregate row; the LEFT JOIN then exposes NULL instead of the original 0. For example, if the middle table has a row with ts=0, WHERE x.ts = (SELECT approx_count(y.ts) FROM y WHERE y.id = outer.id) should match when that outer id has no y rows, but this rewrite cannot match it. Please gate this on the aggregate empty-input contract (or conservatively whitelist only NULL-on-empty aggregates) and add a no-matching-key regression test.
fixed |
fixed |
XuPeng-SH
left a comment
There was a problem hiding this comment.
Deep-reviewed updated exact head 873419c34d7ceab3a25c194c7e1a9503e8314b7c. The previous aggregate/projection/consumer NULL-safety blockers are fixed conservatively, but one structural correctness blocker remains.
P1 — LIMIT/OFFSET become global after per-key decorrelation
The new deepScalarAggregate eligibility checks the aggregate empty-input contract and NULL propagation, but not row-order/row-limit wrappers on the scalar aggregate plan. pullupThroughAgg adds the deep correlation key to GROUP BY, turning the original one-row implicit aggregate into multiple key groups. A LIMIT or OFFSET that originally ran independently inside each correlated scalar invocation then remains on the grouped plan and truncates keys globally.
I reproduced this through SQL on a server built from this exact head using the PR fixture:
shape id=1 id=2 id=3
no LIMIT 220 300 NULL
inner aggregate LIMIT 1 220 NULL NULL
inner LIMIT 1 OFFSET 1 NULL 300 NULL
LIMIT 1 must be equivalent to no limit because an implicit scalar aggregate already returns exactly one row per invocation. LIMIT 1 OFFSET 1 must return no scalar row for every outer id. The observed result shows that limit/offset are applied once across the decorrelated correlation-key groups.
Please make deep-scalar eligibility validate the complete plan topology from the scalar root to AGG. Keep LIMIT/OFFSET (and any other wrapper whose semantics are per correlated invocation but global after grouping) on the NYI path unless it is explicitly rewritten per key. Add public regressions for both LIMIT 1 equivalence and LIMIT 1 OFFSET 1, with multiple matching and missing outer keys.
Fresh exact-head service build, go list, build, vet, focused new tests, and the full pkg/sql/plan suite pass; the blocker is an uncovered reachable plan shape.
fixed |
XuPeng-SH
left a comment
There was a problem hiding this comment.
Deep-reviewed updated exact head 25aca5e219f4df1b5efa69c6983380d118a7ddfa. The previous LIMIT/OFFSET blocker is closed by a conservative topology proof: only PROJECT* -> AGG(aggregateTag) is eligible, LIMIT/OFFSET/RANK are rejected on every accepted node, and SORT/DISTINCT/FILTER/other wrappers remain on the NYI path.
Exact-head public SQL verification covered:
- the supported direct aggregate and CAST-projection shapes, both returning
1 -> 220,2 -> 300,3 -> NULL; - the earlier missing-group hazards (
APPROX_COUNT, innerCOALESCE) remaining NYI; - LIMIT 1, LIMIT 1 OFFSET 1, DISTINCT, and SORT wrapper controls all remaining NYI.
go list, package build, vet, focused new tests, the full pkg/sql/plan suite, and an exact-head service build all pass. I found no remaining correctness blocker.
Merge Queue Status
This pull request spent 42 minutes 1 second in the queue, with no time running CI. Waiting for
All conditions
ReasonPull request #26480 has been dequeued Pull request from fork cannot be queued. This pull request comes from a fork, and Mergify needs the author's permission to update its branch.
Failing checks:
HintYou should look at the reason for the failure and decide if the pull request needs to be fixed or if you want to requeue it. Tick the box to put this pull request back in the merge queue (same as
|
What type of PR is this?
Which issue(s) this PR fixes:
issue #24997
What this PR does / why we need it:
fix(plan): support nested correlated scalar aggregates