pingcap / pingcap/tidb

Incorrect MIN/MAX Result Due to TiKV TopN Push-down with CAST / BINARY Expression

Open
#69,177 3 comments 0 reactions 0 assignees View on GitHub
component/tikv contribution first-time-contributor 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)
case 1

~~~sql
DROP DATABASE IF EXISTS repro39;
CREATE DATABASE repro39 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
USE repro39;

CREATE TABLE src (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
c0 CHAR(1)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

INSERT INTO src(c0) VALUES ('-'), ('(');

CREATE TABLE l (
id BIGINT NOT NULL PRIMARY KEY,
c0 CHAR(1)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE TABLE r (
id BIGINT NOT NULL PRIMARY KEY,
c0 CHAR(1)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

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

SELECT HEX(MIN(CAST(c0 AS BINARY))) AS source_hex,
MIN(CAST(c0 AS BINARY)) AS source_val
FROM src;

SELECT HEX(MIN(CAST(l.c0 AS BINARY))) AS join_hex,
MIN(CAST(l.c0 AS BINARY)) AS join_val
FROM l JOIN r ON l.id = r.id;
~~~

case 2

~~~sql
DROP DATABASE IF EXISTS repro41;
CREATE DATABASE repro41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
USE repro41;

CREATE TABLE src (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
c0 CHAR(1)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

INSERT INTO src(c0) VALUES (''), ('璘');

CREATE TABLE l (
id BIGINT NOT NULL PRIMARY KEY
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE TABLE r (
id BIGINT NOT NULL PRIMARY KEY,
c0 CHAR(1)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

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

SELECT HEX(MAX(BINARY c0)) AS single_hex,
MAX(BINARY c0) AS single_val
FROM src;

SELECT HEX(MAX(BINARY r.c0)) AS join_hex,
MAX(BINARY r.c0) AS join_val
FROM l JOIN r ON l.id = r.id;
~~~

### 2. What did you expect to see? (Required)

**Case 1:** The binary minimum of ( (0x28) and - (0x2D) is ( (0x28).
Both single-table and join queries should return 0x28 and the character (.

**Case 2:** The binary maximum of  (0xEEAC86) and 璘 (0xEFA7AF) is 璘 (0xEFA7AF).
Both single-table and join queries should return 0xEFA7AF and the character 璘.

### 3. What did you see instead (Required)

Single-table query returns incorrect result:

Case 1: source_hex = 2D, source_val = - (should be ()

Case 2: single_hex = EEAC86, single_val =  (should be 璘)

Join query returns correct result as expected in both cases.

### 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)** – TopN is pushed down to TiKV (cop[tikv]):

~~~sql
TopN_20 [cop[tikv]] cast(repro39.src.c0, var_string(1) BINARY), offset:0, count:1
~~~

**Join query (correct)** – TopN stays at TiDB root:

~~~sql
TopN_20 [root] Column#10, offset:0, count:1
~~~

### 6. Root Cause

TiDB optimizes MIN(expr) and MAX(expr) into TopN(expr, [asc|desc], 1). When expr includes a type cast to binary (e.g., CAST(c0 AS BINARY) or BINARY c0), and the TopN operator is pushed down to TiKV, TiKV’s expression evaluation engine produces an incorrect sort order, resulting in wrong aggregate results.

The bug is consistently reproducible in single-table queries. In multi-table JOIN queries, the TopN remains at the TiDB root level, avoiding the faulty TiKV evaluation and yielding correct results.

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.