pingcap / pingcap/tidb

`ORDER BY`-Triggered Aggregation Push‑Down Causes TiKV to Pad `CAST(... AS TIME)` Result, Inflating `BIT_LENGTH()`

Open
#69,308 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 repro_tidb617_1_db18_min;
CREATE DATABASE repro_tidb617_1_db18_min;
USE repro_tidb617_1_db18_min;

SET SESSION sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
SET time_zone='+00:00';

CREATE TABLE src(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
c0 DECIMAL(65,30) NULL
);

INSERT INTO src(c0) VALUES (0);

CREATE TABLE l(
id BIGINT NOT NULL PRIMARY KEY,
c0 DECIMAL(65,30) NULL
);

CREATE TABLE r(
id BIGINT NOT NULL PRIMARY KEY
);

INSERT INTO l SELECT id, c0 FROM src;
INSERT INTO r SELECT id FROM src;

-- Single‑table with ORDER BY: returns 80
SELECT MAX(
BIT_LENGTH(
CASE c0
WHEN ((BINARY id) | (id NOT LIKE c0)) THEN NULL
ELSE CAST(0.7772428249950747 AS TIME)
END
)
) AS single_val
FROM src
WHERE 1
ORDER BY id;

-- Join/rewrite: returns 64
SELECT MAX(
BIT_LENGTH(
CASE v.c0
WHEN ((BINARY v.id) | (v.id NOT LIKE v.c0)) THEN NULL
ELSE CAST(0.7772428249950747 AS TIME)
END
)
) AS split_val
FROM (
SELECT l.id, l.c0
FROM l JOIN r ON l.id = r.id
) v
WHERE 1
ORDER BY v.id;
```

### 2. What Did You Expect to See? (Required)

Row‑by‑row evaluation at the TiDB root layer shows that the `CASE` expression returns `'00:00:01'` (a string of length 8), and `BIT_LENGTH('00:00:01')` = 64. Therefore `MAX(BIT_LENGTH(...))` should be 64 for both the single‑table and the join query.

### 3. What Did You See Instead? (Required)

- Single‑table query with `ORDER BY id`: returns `80` (wrong)
- Join query: returns `64` (correct)
- Single‑table query without `ORDER BY id`: also returns `64` — the bug disappears when `ORDER BY` is removed.

### 4. What Is Your TiDB Version? (Required)

Version: TiDB‑v9.0.0

### 5. Execution Plan Differences

**Single‑table with `ORDER BY id` (wrong)** — aggregation pushed down to TiKV:

```
Sort_9 [root]
└─HashAgg_17 [root] funcs:max(Column#6)->Column#4, firstrow(Column#7)->src.id
└─TableReader_18
└─HashAgg_11 [cop[tikv]] funcs:max(bit_length(cast(case(..., NULL, 00:00:01), var_string(10))))->Column#6
└─TableFullScan_16 [cop[tikv]]
```

The `CAST(... AS TIME)` result is internally typed as `var_string(10)` inside TiKV, giving `BIT_LENGTH` = 80.

**Join query (correct)** — expression evaluated at TiDB root before aggregation:

```
Sort_14 [root]
└─HashAgg_18 [root] funcs:max(Column#8)->Column#6
└─Projection_63 [root] bit_length(cast(case(..., , 00:00:01), var_string(10)))->Column#8
└─MergeJoin_21 [root]
```

At the root layer the same `var_string(10)` cast yields a string of 8 characters, so `BIT_LENGTH` correctly returns 64.

### 6. Root Cause

The `ORDER BY id` clause in an aggregate query without `GROUP BY` forces the optimizer to preserve the non‑aggregated column `id`. This triggers a two‑phase aggregation strategy where partial `MAX(BIT_LENGTH(...))` and `firstrow(id)` are pushed down to TiKV.

When TiKV evaluates `CAST(0.7772428249950747 AS TIME)`, it stores the result as a `var_string(10)` — i.e., a fixed‑width 10‑byte string, padding the time value `'00:00:01'` to `'00:00:01 '` (or similar), thus `BIT_LENGTH` = 80. In contrast, TiDB's root expression evaluator treats the same cast as a normal variable‑length string with 8 characters, yielding `BIT_LENGTH` = 64.

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.