Inconsistent COUNT Result on CAST(BOOL AS TIME AS DATETIME) When Aggregation Is Pushed Down to TiKV
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
## Bug Report
Please answer these questions before submitting your issue. Thanks!
### 1. Minimal reproduce step (Required)
~~~sql
DROP DATABASE IF EXISTS repro18_min;
CREATE DATABASE repro18_min DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
USE repro18_min;
CREATE TABLE src (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
c0 BOOL
);
INSERT INTO src(c0) VALUES (false), (true);
CREATE TABLE l (
id BIGINT NOT NULL PRIMARY KEY
);
CREATE TABLE r (
id BIGINT NOT NULL PRIMARY KEY,
c0 BOOL
);
INSERT INTO l SELECT id FROM src;
INSERT INTO r SELECT id, c0 FROM src;
-- Single-table query (returns wrong count)
SELECT COUNT(CAST(
CASE WHEN FALSE
THEN c0
ELSE CAST(c0 AS TIME)
END AS DATETIME
)) AS single_cnt
FROM src;
-- Join query (returns correct count)
SELECT COUNT(CAST(
CASE WHEN FALSE
THEN r.c0
ELSE CAST(r.c0 AS TIME)
END AS DATETIME
)) AS join_cnt
FROM l JOIN r ON l.id = r.id;
~~~
### 2. What did you expect to see? (Required)
Both `single_cnt` and `join_cnt` should return `1`. The boolean value `true` (1) cannot be meaningfully converted to a valid `TIME` without truncation or error; when subsequently cast to `DATETIME` the expression should evaluate to `NULL`, and `COUNT()` ignores `NULL`. The `false` (0) row converts to `TIME '00:00:00'` → `DATETIME '0000-00-00 00:00:00'` (a non‑NULL value), so the overall count of non‑NULL results is 1.
### 3. What did you see instead (Required)
- `single_cnt` = 2 (incorrect)
- `join_cnt` = 1 (correct)
The join query additionally returns a warning: `Truncated incorrect time value: '1'`.
### 4. What is your TiDB version? (Required)
I tested such a case in TiDB-v8.5.6. and TiDB-v9.0.0. Maybe it is an issue that exists in all the versions.
## 5. Execution Plan Differences
**Single-table query (wrong)** – Aggregation pushed down to TiKV (`cop[tikv]`):
```
StreamAgg_16 [root] funcs:count(Column#6)->Column#4
└─TableReader_17 [root] data:StreamAgg_8
└─StreamAgg_8 [cop[tikv]] funcs:count(cast(cast(cast(src.c0, time BINARY), var_string(10)), datetime BINARY))->Column#6 ← ⚠️ pushed down
└─TableFullScan_15 [cop[tikv]] table:src
```
The entire `CAST` chain and `COUNT` are evaluated inside TiKV's coprocessor.
**Join query (correct)** – Expression evaluated at TiDB root, aggregation at root:
```
StreamAgg_13 [root] funcs:count(Column#7)->Column#6
└─Projection_56 [root] cast(cast(cast(r.c0, time BINARY), var_string(10)), datetime BINARY)->Column#7 ← ✅ executes at root
└─IndexJoin_42 [root] inner join, outer key:l.id, inner key:r.id
├─TableReader_34 [root] table:l
└─TableReader_36 [root] table:r
```
The projection containing the casts runs at the TiDB layer, not on TiKV.
## 6. Root Cause
The query applies a double type cast: `BOOL` → `TIME` → `DATETIME`.
- For `c0 = false (0)`: both TiKV and TiDB produce `CAST(0 AS TIME) = '00:00:00'`, which further casts to a valid `DATETIME`.
- For `c0 = true (1)`: TiDB's SQL layer treats `CAST(1 AS TIME)` as an invalid time value, generates a warning, and returns `NULL`. The outer `CAST(NULL AS DATETIME)` remains `NULL`, and `COUNT()` discards it. Therefore the correct count is 1. In TiKV's coprocessor, however, `CAST(1 AS TIME)` appears to succeed and returns `'00:00:01'` (or a non‑error value), which then becomes a non‑NULL `DATETIME`. Consequently TiKV counts both rows, yielding 2.
Because the single‑table query allows the entire aggregation expression to be pushed down to TiKV, the wrong evaluation logic is used. The join query prevents push‑down of the aggregation (it must be executed after the join), so the expression is evaluated by TiDB's own expression engine, producing the correct result.
Contributor guide
Assessment
This issue has not been assessed yet.