Skip to content

fix(optimizer): incorrect merge with literal alias collision#7847

Open
fivetran-kwoodbeck wants to merge 3 commits into
mainfrom
optimizer/bug-literal-group-projection
Open

fix(optimizer): incorrect merge with literal alias collision#7847
fivetran-kwoodbeck wants to merge 3 commits into
mainfrom
optimizer/bug-literal-group-projection

Conversation

@fivetran-kwoodbeck

Copy link
Copy Markdown
Collaborator

Merging a derived table that projects a numeric literal (6 AS a) whose alias matches a FROM column, and is used in the outer GROUP BY, rewrites it to a bare GROUP BY a that rebinds to the column instead of the constant. This changes the query result. The fix is that _mergeable now blocks those merges.

Sample query:

SELECT s.a, SUM(s.b) AS b FROM (SELECT 6 AS a, b FROM x) AS s GROUP BY s.a;

Before, this query will return different results:

SELECT 6 AS a, SUM(x.b) AS b FROM x AS x GROUP BY a;

After:

SELECT s.a AS a, SUM(s.b) AS b FROM (SELECT 6 AS a, x.b AS b FROM x AS x) AS s GROUP BY s.a;

@fivetran-kwoodbeck fivetran-kwoodbeck changed the title fix(optimizer): issue on GROUP BY when literal aliases colliding fix(optimizer): incorrect merge with literal alias collision Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

SQLGlot Integration Test Results

✅ All tests passed

Comparing:

  • this branch (sqlglot:optimizer/bug-literal-group-projection @ sqlglot d15d606)
  • baseline (main @ sqlglot 122b44f)

Overall

main: 192416 total, 153532 passed (pass rate: 79.8%)

sqlglot:optimizer/bug-literal-group-projection: 180222 total, 142385 passed (pass rate: 79.0%)

Transitions:
No change

Dialect pair changes: 0 previous results not found, 3 current results not found

✅ All tests passed

@georgesittas georgesittas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried optimizing this query as well

-- Input
WITH c AS (SELECT DISTINCT x.b AS b FROM x)
SELECT s.a, SUM(s.b) AS b FROM (SELECT 6 AS a, b FROM c) AS s CROSS JOIN x GROUP BY s.a

-- Optimized under this PR
WITH c AS (...) SELECT 6 AS a, SUM(c.b) AS b FROM c CROSS JOIN x GROUP BY a

The GROUP BY now binds a to x.a from the outer cross join. Seems like a similar bug. Additionally, there appears to be a similar bug with ORDER BY on a non-projected literal alias:

-- input
SELECT SUM(s.b) AS b FROM (SELECT 6 AS a, b FROM x) AS s GROUP BY s.a ORDER BY s.a

-- Optimized
SELECT SUM(x.b) AS b FROM x AS x GROUP BY a ORDER BY 6

This is also problematic, but let's address it separately to avoid scope creep.

Comment thread sqlglot/optimizer/merge_subqueries.py Outdated
Comment thread sqlglot/optimizer/merge_subqueries.py Outdated
Comment on lines +162 to +164
literal_aliases = {s.alias_or_name for s in inner_select.selects if s.unalias().is_number}
if not literal_aliases:
return False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be careful not to walk selects / expressions more than needed. This can be wasteful if repeated without care. It's worth looking into refactoring this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we can get around this one.

Comment on lines +166 to +170
grouped = {
c.name
for c in group.find_all(exp.Column)
if c.table == inner_name and c.name in literal_aliases
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, these find / find_all calls can result in walking many nodes. There are other places where they happen in _mergeable. We should make sure we're not repeating a lot of work.

There may be a better alternative of pre-aggregating everything we need for these checks, but for that we would need to run all the cheap "mergeable" checks first, to avoid taking on the overhead preemptively.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

found a way around the find_all

Comment thread sqlglot/optimizer/merge_subqueries.py Outdated
Comment on lines +176 to +178
if isinstance(source, Scope) and grouped & {
s.alias_or_name for s in source.expression.selects
}:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about:

Suggested change
if isinstance(source, Scope) and grouped & {
s.alias_or_name for s in source.expression.selects
}:
if isinstance(source, Scope) and grouped & set(source.expression.named_selects):

@fivetran-kwoodbeck fivetran-kwoodbeck force-pushed the optimizer/bug-literal-group-projection branch from a5a7cfe to 283107e Compare July 9, 2026 20:07
@fivetran-kwoodbeck fivetran-kwoodbeck force-pushed the optimizer/bug-literal-group-projection branch from 283107e to 4f3280b Compare July 10, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants