Search before asking
Version
8.0.90-v1.2.925-patch-10-1abb4b1e29 (rust nightly 2026-08-25) — the latest patch release as of 2026-08-25. Reproduced 3/3 runs.
What's Wrong?
A reflexive aggregate comparison MIN(col) = MIN(col) is NULL-typed under SQL three-valued logic: for a group whose aggregate result is NULL, the comparison evaluates to NULL and the group must be filtered out by HAVING.
Databend keeps the group when the comparison sits in the HAVING clause, and drops it correctly when the identical expression is placed in a projection column of a derived table and filtered afterwards:
| Query form |
Groups returned (for the reproducer below) |
Verdict |
HAVING (MIN(col) = MIN(col)) |
3 (includes the all-NULL group) |
wrong |
projection column + outer WHERE |
2 |
correct |
This was discovered by SQLancer's DQR (Derived Query Relocation) equivalence oracle: it relocates a HAVING predicate into a derived-table projection and asserts both forms return the same rows. They do not — the HAVING form retains the NULL-aggregate groups.
How to Reproduce?
CREATE TABLE t0(c0timestamp TIMESTAMP NULL);
INSERT INTO t0 VALUES ('2020-01-01 10:00:00'), ('2020-05-05 08:30:00'), (NULL);
-- HAVING form: returns THREE rows, including the all-NULL group
SELECT t0.c0timestamp FROM t0
GROUP BY t0.c0timestamp
HAVING (MIN(t0.c0timestamp) = MIN(t0.c0timestamp));
-- 2020-01-01 10:00:00.000000
-- 2020-05-05 08:30:00.000000
-- NULL <-- WRONG: MIN is NULL, NULL = NULL is NULL, group must be dropped
-- Projection form (same expression, same grouping): returns TWO rows
SELECT ref0 FROM (
SELECT t0.c0timestamp AS ref0,
(MIN(t0.c0timestamp) = MIN(t0.c0timestamp)) AS ref1
FROM t0 GROUP BY t0.c0timestamp
) AS s WHERE ref1;
-- 2020-01-01 10:00:00.000000
-- 2020-05-05 08:30:00.000000 <-- correct
MAX behaves identically to MIN. The divergence also reproduces with GROUP BY GROUPING SETS ((col), ()) and a conjoined GROUPING(col) = 0 atom — the super-aggregate context does not change the outcome.
Cluster information: single node, local datafuselabs/databend Docker container (MySQL protocol). No special configuration.
Additional analysis
- A bare column self-comparison
HAVING (c0timestamp = c0timestamp) behaves identically in BOTH positions (both keep the NULL group), so the HAVING path mishandles x = x with two-valued logic generally — but only the aggregate form diverges from the projection path, which is what makes the defect observable as a wrong result.
- The projection/materialization path produces NULL for NULL inputs and correctly filters the group; the
HAVING filter path does not.
Are you willing to submit PR?
Search before asking
Version
8.0.90-v1.2.925-patch-10-1abb4b1e29(rust nightly 2026-08-25) — the latest patch release as of 2026-08-25. Reproduced 3/3 runs.What's Wrong?
A reflexive aggregate comparison
MIN(col) = MIN(col)is NULL-typed under SQL three-valued logic: for a group whose aggregate result is NULL, the comparison evaluates to NULL and the group must be filtered out byHAVING.Databend keeps the group when the comparison sits in the
HAVINGclause, and drops it correctly when the identical expression is placed in a projection column of a derived table and filtered afterwards:HAVING (MIN(col) = MIN(col))WHEREThis was discovered by SQLancer's DQR (Derived Query Relocation) equivalence oracle: it relocates a
HAVINGpredicate into a derived-table projection and asserts both forms return the same rows. They do not — theHAVINGform retains the NULL-aggregate groups.How to Reproduce?
MAXbehaves identically toMIN. The divergence also reproduces withGROUP BY GROUPING SETS ((col), ())and a conjoinedGROUPING(col) = 0atom — the super-aggregate context does not change the outcome.Cluster information: single node, local
datafuselabs/databendDocker container (MySQL protocol). No special configuration.Additional analysis
HAVING (c0timestamp = c0timestamp)behaves identically in BOTH positions (both keep the NULL group), so the HAVING path mishandlesx = xwith two-valued logic generally — but only the aggregate form diverges from the projection path, which is what makes the defect observable as a wrong result.HAVINGfilter path does not.Are you willing to submit PR?