fix: Do not mark GROUP BY derived unique keys as nullable#23636
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #23636 +/- ##
=======================================
Coverage ? 80.65%
=======================================
Files ? 1086
Lines ? 366106
Branches ? 366106
=======================================
Hits ? 295279
Misses ? 53234
Partials ? 17593 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
GROUP BY treats NULLs as equal, so every key combination -- NULL included -- occurs in exactly one output row, like a primary key. Marking the dependency nullable (multiple NULL keys may coexist) was overly conservative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`ReplaceDistinctWithAggregate` removes `Distinct::All` when a
`Dependency::Single` functional dependence covers all input fields.
However a nullable UNIQUE constraint also produces a `Single`-mode
dependence, and SQL UNIQUE permits multiple NULL keys. DISTINCT treats
NULLs as equal, so it must still collapse those rows:
CREATE TABLE t (x INT UNIQUE) AS VALUES (NULL), (NULL), (1);
SELECT DISTINCT x FROM t;
returned two NULL rows.
Only remove the DISTINCT when the dependence is non-nullable or none of
its source fields are actually nullable, mirroring the guards already
used by `EliminateJoin` and `Filter::is_scalar`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8b4bc1b to
2b62f29
Compare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
This is a nice fix, although I notice there are a few other places that consume nullable FDs incorrectly:
get_required_group_by_exprs_indices-- we had a fix in-process for this in #21715 but it was never landed.get_required_sort_exprs_indices-- similar to (1)is_ordering_strictfor window frames
If you like I can take on this followup work? Might make sense to add a helper to unify the logic here, but probably easier to do in the followup PR.
| // Add a new functional dependency associated with the whole table. | ||
| // | ||
| // `nullable` is `false`: a nullable dependence means multiple NULL | ||
| // keys may coexist (as under a SQL UNIQUE constraint, where NULLs | ||
| // compare distinct). That cannot happen after grouping: GROUP BY | ||
| // treats NULLs as equal, so every key combination -- NULL included | ||
| // -- occurs in exactly one output row, like a primary key. |
There was a problem hiding this comment.
Is it possible to write a unit test to check exactly this property?
| let source_indices = &dep.source_indices; | ||
| let any_source_field_nullable = source_indices | ||
| .iter() | ||
| .any(|&idx| schema.field(idx).is_nullable()); | ||
| let nullable = dep.nullable && any_source_field_nullable; | ||
| if !nullable | ||
| && dep.mode == Dependency::Single | ||
| && source_indices.len() >= field_count | ||
| && source_indices[..field_count] |
There was a problem hiding this comment.
Similarly, I wonder if we can write a unit test to check this property directly?
| // The grouping columns must also not contain NULLs because | ||
| // a nullable UNIQUE constraint permits multiple NULL keys, | ||
| // but DISTINCT treats NULLs as equal and must still | ||
| // collapse them. |
There was a problem hiding this comment.
This comment seems a bit opaque to me (e.g., I don't think "grouping columns" is the right phrase). How about:
// A nullable dependency allows multiple rows with NULL in
// their source columns. DISTINCT treats NULLs as equal and
// collapses them, so we can only remove the DISTINCT if we
// know that no source column can actually be NULL.
Which issue does this PR close?
Rationale for this change
This query is wrong
What changes are included in this PR?
Fix bug by correctly computing and consuming
FunctionalDependenciesAre these changes tested?
Yes, new sqllogictest cases in
group_by.sltcover the wrong-results query, the plan for the nullableUNIQUEcase (theDISTINCTis planned as anAggregate), and thatDISTINCTover aPRIMARY KEYis still removed.Are there any user-facing changes?
No API changes.
SELECT DISTINCTover a table with a nullableUNIQUEconstraint now returns correct results (at the cost of keeping the aggregation in the plan).