Skip to content

Decorrelate subqueries whose grouping sets leave out the correlated column #25708

Description

@jayzhan211

Is your feature request related to a problem or challenge?

Follow-up to #25529 (fix for #25519).

To stop wrong results, #25529 keeps a correlated filter below an aggregate with a grouping set unless every set already groups by each column the pull up would add. That guard is conservative: it also rejects non-empty sets that leave out the correlated column. On main those queries were answered correctly, and with #25529 they fail to plan:

CREATE TABLE o(k INT) AS VALUES (1), (2), (NULL), (4), (5);
CREATE TABLE i(k INT, j INT) AS VALUES (1, 10), (NULL, 20), (5, 30), (2, 40);

SELECT o.k FROM o
WHERE EXISTS (
  SELECT 1 FROM i WHERE i.k = o.k
  GROUP BY GROUPING SETS ((i.k), (i.j))
)
ORDER BY o.k;
-- with #25529: This feature is not implemented: Physical plan does not support logical expression Exists(...)

Because the filter i.k = o.k fixes i.k to one value per outer row, adding i.k to a non-empty set that lacks it leaves the rows of each outer row unchanged. Only two things change: the value of i.k in those rows (NULL becomes o.k) and __grouping_id. So the old rewrite was wrong only when something above the aggregate reads the NULL-filled column or GROUPING(). One such case is HAVING i.k IS NULL, which is covered in subquery.slt.

Describe the solution you'd like

Decorrelate these queries again without bringing back the wrong results. Two options:

  1. Add the correlated column to each set that lacks it under an alias, so the column the query reads keeps its NULL fill and the join key is a separate column.
  2. Reject only when a set is empty (ROLLUP/CUBE always contain one) or when a node above the aggregate reads the NULL-filled column or GROUPING().

Empty sets must stay rejected: they yield a row for outer rows that match nothing, and a join cannot produce that row.

Describe alternatives you've considered

Keep the current guard. The queries fail to plan instead of returning wrong results.

Additional context

The guard is in the Aggregate arm of PullUpCorrelatedExpr::f_up, datafusion/optimizer/src/decorrelate.rs. The known limitation is documented in datafusion/sqllogictest/test_files/subquery.slt.

Activity

  1. namanjain24-sudo commented on Sep 24, 2026

    @namanjain24-sudo
    Contributor

    take

  2. mohitgurav20 commented on Sep 24, 2026

    @mohitgurav20
    Contributor

    Thanks for opening this issue @jayzhan211! The follow-up context from #25529 / #25519 makes complete sense.

    Technical Analysis & Observations

    1. Why the Current Guard is Overly Conservative:

      • In fix: keep a correlated filter below an aggregate with a grouping set #25529, rejecting decorrelation whenever any grouping set omits the correlated column was necessary to prevent wrong results when expressions above Aggregate read the NULL-filled column (e.g. HAVING i.k IS NULL) or evaluate GROUPING(i.k).
      • However, for predicates like WHERE EXISTS (SELECT 1 FROM i WHERE i.k = o.k GROUP BY GROUPING SETS ((i.k), (i.j))), the correlated filter i.k = o.k pins i.k to a single outer value o.k. For non-empty grouping sets like (i.j), pulling up i.k does not alter row counts per outer row—it only changes the projected i.k value in that grouping set from NULL to o.k.
      • Because EXISTS (and subqueries where upper nodes do not read the NULL-filled column or GROUPING()) only checks row existence, decorrelating to a join is entirely sound and yields correct results.
    2. Empty Grouping Sets () Must Remain Guarded:

      • As noted, empty grouping sets () (such as in grand-total aggregations or CUBE/ROLLUP with empty sets) yield 1 output row even when the subquery input is empty. A standard inner/left join cannot produce a row on an empty match without unmatched row indicator handling (the count bug mechanism). Thus, empty grouping sets () must stay rejected.
    3. Comparison of Solution Approaches:

      • Option 1 (Aliasing correlated key per grouping set): Clean and preserves NULL semantics for expressions inspecting i.k, but requires rewriting grouping set expressions inside Aggregate.
      • Option 2 (Refined guard check in PullUpCorrelatedExpr): Checks whether any upper node in the subquery reads the correlated column or GROUPING() expression. If not (such as in EXISTS or standard projections that don't output the correlated column), decorrelation proceeds safely by expanding non-empty grouping sets.

    Proposed Implementation Plan

    1. Refine Guard in PullUpCorrelatedExpr::f_up (datafusion/optimizer/src/decorrelate.rs):

      • Inspect LogicalPlan::Aggregate in PullUpCorrelatedExpr.
      • Ensure no empty grouping set () exists in group_expr.
      • Validate whether parent projection/having nodes reference the correlated column or GROUPING() expressions before deciding can_pull_up.
    2. Testing & Verification:

      • Add unit tests in decorrelate.rs and SQL logictests in datafusion/sqllogictest/test_files/subquery.slt covering:
        • EXISTS subqueries with GROUPING SETS ((i.k), (i.j)) (re-enabling decorrelation).
        • Subqueries with HAVING i.k IS NULL or GROUPING(i.k) (verifying they safely fall back or remain guarded).
        • Empty grouping sets GROUPING SETS ((), (i.k)) ensuring correct rejection.
      • Run standard lint suite (cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, ./dev/rust_lint.sh) and subquery test suite.

    I would like to work on this issue!

    take

  3. namanjain24-sudo commented on Sep 24, 2026

    @namanjain24-sudo
    Contributor

    Thanks @mohitgurav20 — looks like we grabbed this within the same minute, I'd taken it just before your comment landed, so I'll run with it. Appreciate the analysis though.

  4. mohitgurav20 commented on Sep 24, 2026

    @mohitgurav20
    Contributor

    Thanks a lot for the warm reply @namanjain24-sudo! Absolutely no problem at all — timing was super close!

    Since I had already spent time digging into decorrelate.rs and setting up local test cases for Option 2, I'd really love to contribute to this fix. Would you be open to collaborating on this or working together on the PR (e.g. co-authoring)? Otherwise, I'd be more than happy to review your PR when it's ready!

    Either way, appreciate the awesome community spirit!

  5. namanjain24-sudo commented on Sep 24, 2026

    @namanjain24-sudo
    Contributor

    I'll take it solo for now since I've already got the context built up, but a review once the PR's up would be great — will ping you here.

  6. mohitgurav20 commented on Sep 24, 2026

    @mohitgurav20
    Contributor

    yeahh suree !..

  7. namanjain24-sudo commented on Sep 26, 2026

    @namanjain24-sudo
    Contributor

    PR up: #25781. @mohitgurav20 would appreciate a review when you get a chance.

  8. added 3 commits that reference this issue on Sep 30, 2026
    6917189
    da9f7b7
    42adbba
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions