HAVING and equivalent WHERE on derived table return different rows with BIT_OR, CASE and FLOAT UNSIGNED
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
## Bug Report
### Bug Description
Two logically equivalent queries produce different results. The first query applies a predicate in the HAVING clause; the second wraps the same predicate in a derived table and applies it via WHERE. Both have identical execution plans (same operators, same expressions), yet one returns 0 rows and the other returns 1 row.
This indicates that TiDB evaluates the same expression tree differently depending on whether it is compiled as a HAVING condition or as a WHERE condition over a derived table.
Please answer these questions before submitting your issue. Thanks!
### 1. Minimal reproduce step (Required)
```sql
CREATE TABLE t0(
c0 NUMERIC UNSIGNED,
c1 FLOAT UNSIGNED,
c2 FLOAT UNSIGNED
);
INSERT INTO t0 VALUES (1443408820, 0.34189489665228545, 0.36490945335401803);
-- Query A: predicate in HAVING
-- Returns 0 rows
SELECT CONCAT(IFNULL(t0.c2, '__NULL__'))
FROM t0
GROUP BY t0.c2
HAVING (
((BIT_OR(t0.c2)) > (AVG(t0.c2)))
OR
((t0.c2) > (
(CAST(TRIM((CASE t0.c2 WHEN t0.c2 THEN t0.c2 ELSE 1923309721 END)) AS CHAR)) >= (t0.c2)
))
);
-- Query B: same predicate in derived table WHERE
-- Returns 1 row
SELECT ref0
FROM (
SELECT
CONCAT(IFNULL(t0.c2, '__NULL__')) AS ref0,
(
((BIT_OR(t0.c2)) > (AVG(t0.c2)))
OR
((t0.c2) > (
(CAST(TRIM((CASE t0.c2 WHEN t0.c2 THEN t0.c2 ELSE 1923309721 END)) AS CHAR)) >= (t0.c2)
))
) AS ref1
FROM t0
GROUP BY t0.c2
) AS s
WHERE ref1;
```
### 2. What did you expect to see? (Required)
Both queries must return the same result set, just like in MySQL returning 1 row (0.364909).
### 3. What did you see instead (Required)
```shell
mysql> SELECT CONCAT(IFNULL(t0.c2, '__NULL__'))
-> FROM t0
-> GROUP BY t0.c2
-> HAVING (
-> ((BIT_OR(t0.c2)) > (AVG(t0.c2)))
-> OR
-> ((t0.c2) > (
-> (CAST(TRIM((CASE t0.c2 WHEN t0.c2 THEN t0.c2 ELSE 1923309721 END)) AS CHAR)) >= (t0.c2)
-> ))
-> );
Empty set (0.00 sec)
mysql> SELECT ref0
-> FROM (
-> SELECT
-> CONCAT(IFNULL(t0.c2, '__NULL__')) AS ref0,
-> (
-> ((BIT_OR(t0.c2)) > (AVG(t0.c2)))
-> OR
-> ((t0.c2) > (
-> (CAST(TRIM((CASE t0.c2 WHEN t0.c2 THEN t0.c2 ELSE 1923309721 END)) AS CHAR)) >= (t0.c2)
-> ))
-> ) AS ref1
-> FROM t0
-> GROUP BY t0.c2
-> ) AS s
-> WHERE ref1;
+------------+
| ref0 |
+------------+
| 0.36490944 |
+------------+
1 row in set (0.00 sec)
```
### 4. What is your TiDB version? (Required)
**Two versions: v8.5.5 and v8.5.6.**
```shell
mysql> select tidb_version();
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| tidb_version() |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Release Version: v8.5.6
Edition: Community
Git Commit Hash: ae18096e023780bb56bfce33698abec0d4640d0a
Git Branch: HEAD
UTC Build Time: 2026-04-24 09:13:10
GoVersion: go1.25.8
Race Enabled: false
Check Table Before Drop: false
Store: unistore |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> select tidb_version();
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| tidb_version() |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Release Version: v8.5.5
Edition: Community
Git Commit Hash: 1fa258b833ff113883beeba40bc130be7ce66610
Git Branch: HEAD
UTC Build Time: 2026-01-14 22:20:57
GoVersion: go1.25.5
Race Enabled: false
Check Table Before Drop: false
Store: unistore |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```
### Root Cause Analysis by Kimi agent
Since the execution plans are structurally identical and the Selection condition text is the same, the discrepancy must come from how TiDB builds or evaluates the expression tree for HAVING versus WHERE on a derived table.
Possible causes:
(1) Different constant folding: The CASE expression or the CAST chain may be folded differently during plan generation for HAVING vs. derived-table WHERE.
(2) Different type inference: The intermediate result type of BIT_OR(...) or the comparison >= may be inferred differently, affecting the final boolean result.
(3) Different null handling or unsigned semantics: FLOAT UNSIGNED combined with BIGINT UNSIGNED BINARY in BIT_OR may have edge cases in one code path but not the other.
Contributor guide
Assessment
This issue has not been assessed yet.