pingcap / pingcap/tidb

Inconsistent COUNT Result on CAST(BOOL AS TIME AS DATETIME) When Aggregation Is Pushed Down to TiKV

Open
#69,178 2 comments 0 reactions 0 assignees View on GitHub
component/tikv contribution may-affects-7.5 may-affects-8.1 may-affects-8.5 severity/critical type/bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.