unnest_subqueries produces wrong results for COALESCE(SUM(...), k) and for nullable CASE expressions containing COUNT
Maintainers usually reply within 1 day
@treysp is already working on this.
Since Oct 23, 2026.
Assessment
This issue has not been assessed yet.
Description
Description
sqlglot/optimizer/unnest_subqueries.py:366-375 compensates for empty groups only when the scalar subquery
contains exp.Count:
if find_in_scope(value, exp.Count):
def remove_aggs(node):
if isinstance(node, exp.Count):
return exp.Literal.number(0)
elif isinstance(node, exp.AggFunc):
return exp.null()
return node
alias = exp.Coalesce(this=alias, expressions=[value.this.transform(remove_aggs)])
This is wrong in both directions:
(a) Missing compensation for non-COUNT aggregates that are non-NULL on empty input
q = "SELECT a.k, (SELECT COALESCE(SUM(b.v), 0) FROM b WHERE b.k = a.k) AS s FROM a"
print(optimize(sqlglot.parse_one(q),
schema={"a": {"k": "INT"}, "b": {"k": "INT", "v": "INT"}}).sql())
# WITH "_u_0" AS (SELECT COALESCE(SUM("b"."v"), 0) AS "_col_0", "b"."k" AS "_u_1"
# FROM "b" GROUP BY "b"."k")
# SELECT "a"."k", "_u_0"."_col_0" FROM "a"
# LEFT JOIN "_u_0" ON "_u_0"."_u_1" = "a"."k"
The COALESCE stays inside the grouped subquery, so an unmatched outer row gets NULL instead of 0.
(b) Over-compensation that overwrites a legitimate matched NULL
q = ("SELECT a.k, (SELECT CASE WHEN COUNT(*) >= 1 THEN SUM(b.v) ELSE 99 END "
"FROM b WHERE b.k = a.k) AS val FROM a")
print(optimize(sqlglot.parse_one(q),
schema={"a": {"k": "INT"}, "b": {"k": "INT", "v": "INT"}}).sql())
# ... COALESCE("_u_0"."_col_0", 99) AS "val" ...
When a.k = 1 matches one row (k=1, v=NULL), the subquery must return NULL (COUNT(*) >= 1 is true,
SUM(NULL) is NULL), but the added outer COALESCE(_u_0._col_0, 99) turns it into 99.
Expected vs Actual
- (a)
a={(1),(2)},b={(1,10)}: expected[(1,10),(2,0)]; actual[(1,10),(2,None)]. - (b)
a={(1)},b={(1,None)}: expected[(1,None)]; actual[(1,99)].
Root cause
The empty-group compensation is computed from the syntactic presence of COUNT rather than from
"what does this scalar subquery evaluate to on zero tuples". A single probe is insufficient: the compensation
must be a per-expression default computed structurally (as Spark's evalSubqueryOnZeroTups does), and the outer
COALESCE must be applied only when the expression's empty-input value is non-NULL.
Suggested fix
Compute the zero-tuple value of the projection: replace every aggregate by its empty-input default and fold the
surrounding expression. If the result is non-NULL, wrap the joined column with COALESCE(col, default);
otherwise leave it untouched. Do not blanket-wrap any expression that merely contains a COUNT.
- Dominant language
- Python
- Stars
- 9.7k
- Forks
- 1.4k
- Avg merge
- 13h 20m
- Merged PRs (30d)
- 217
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from tobymao/sqlglot
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
BigQuery: quoted identifiers escape a backtick by doubling it and leave a backslash unescaped, which BigQuery reads differentlyPossibly taken @ege-arhan claimed this 1 day ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 45/100
Maintainers usually reply within 1 day
-
Hive: UNIONTYPE<...> data type fails to parsePossibly taken @shenhui027-arch claimed this 1 day ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 65/100
Maintainers usually reply within 1 day
-
Optimizer drops LEFT JOIN when a cast collapses distinct join keysPossibly taken @finnhoang05 claimed this 2 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 30/100
Maintainers usually reply within 1 day
-
duckdb <-> sqlite: CAST of a fractional number to INTEGER rounds in DuckDB but truncates in SQLitePossibly taken @simpleqt claimed this today. Open
Difficulty 3/5 1-2 days Newbie friendliness 65/100
tobymao/sqlglot#8853 · 1 comment ·
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
pytest-dev/pyfakefs#1367 · 1 comment ·
Maintainers usually reply within 2 days
-
IFEvalG exclude_word_harder misses the keyword next to punctuation, at the start or end of the text, and in other letter casePossibly taken @damienen claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
allenai/open-instruct#2151 ·
Maintainers usually reply within 1 day
-
feat(plan): support convertorLabels and convertorAffinity in Plan resourcePossibly taken @AmenB claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
RedHatQE/openshift-python-wrapper#2849 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Maintainers usually reply within 25 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
libnativeapi/nativeapi#21 ·