pingcap / pingcap/tidb

TiDB does not simplify `NOT NOT` in a `WHERE` predicate, preventing `LEFT JOIN` right-side pruning

Open
#69,927 3 comments 0 reactions 0 assignees View on GitHub
contribution severity/moderate sig/planner 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!
The two queries below are logically equivalent. The mutated query only adds a double negation around the `WHERE` predicate:

```sql
WHERE a.c4 = 50
WHERE NOT NOT a.c4 = 50
```

The `LEFT JOIN` condition is `a.c4 < 15`. Since the `WHERE` clause requires `a.c4 = 50`, the join condition is always false for the matching left rows. TiDB optimizes the original query by pruning the right side of the `LEFT JOIN` into `TableDual rows:0`.

However, when the same predicate is written as `NOT NOT a.c4 = 50`, TiDB does not apply the same simplification/pruning. The mutated query still scans the right table.

Observed timing over 12 alternating executions:

```text
original median = 1.544 ms
mutated median = 11.629 ms
ratio = 7.53x slower
```

Both queries return the same result:

```text
COUNT(*) = 1
```

### 1. Minimal reproduce step (Required)

```sql
DROP DATABASE IF EXISTS tidb_notnot_leftjoin_min;
CREATE DATABASE tidb_notnot_leftjoin_min;
USE tidb_notnot_leftjoin_min;

CREATE TABLE a(c4 INT);
CREATE TABLE b(x INT);

CREATE TABLE d(n INT);
INSERT INTO d VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9);

INSERT INTO a VALUES (50);

-- 100,000 rows in the right table
INSERT INTO b
SELECT d1.n * 10000 + d2.n * 1000 + d3.n * 100 + d4.n * 10 + d5.n
FROM d AS d1
JOIN d AS d2
JOIN d AS d3
JOIN d AS d4
JOIN d AS d5;

-- Original query
SELECT COUNT(*)
FROM a
LEFT JOIN b ON a.c4 < 15
WHERE a.c4 = 50;

-- Mutated query: logically equivalent double negation
SELECT COUNT(*)
FROM a
LEFT JOIN b ON a.c4 < 15
WHERE NOT NOT a.c4 = 50;

-- Optional: inspect the plan difference
EXPLAIN
SELECT COUNT(*)
FROM a
LEFT JOIN b ON a.c4 < 15
WHERE a.c4 = 50;

EXPLAIN
SELECT COUNT(*)
FROM a
LEFT JOIN b ON a.c4 < 15
WHERE NOT NOT a.c4 = 50;
```

### 2. What did you expect to see? (Required)
TiDB should simplify `NOT NOT predicate` to `predicate` early enough for contradiction detection and `LEFT JOIN` right-side pruning. The two equivalent queries should have similar execution plans and similar execution time.
### 3. What did you see instead (Required)

Original query:

```text
StreamAgg_9 funcs:count(1)->Column#5
HashJoin_16 CARTESIAN left outer join
TableDual_15(Build) rows:0
TableReader_14(Probe) data:Selection_13
Selection_13 eq(a.c4, 50)
TableFullScan_12 table:a
```

Query with `NOT NOT`:

```text
HashAgg_8 funcs:count(1)->Column#5
HashJoin_11 CARTESIAN left outer join, left cond:[lt(a.c4, 15)]
TableReader_14(Build) data:Selection_13
Selection_13 eq(a.c4, 50)
TableFullScan_12 table:a
TableReader_16(Probe) data:TableFullScan_15
TableFullScan_15 table:b
```

### 4. What is your TiDB version? (Required)

```text
Release Version: v8.5.7
Edition: Community
Git Commit Hash: 202b7f47286a1109b5c957401d34c9358d130ae0
Git Branch: HEAD
UTC Build Time: 2026-07-15 02:06:00
GoVersion: go1.25.10
Race Enabled: false
Check Table Before Drop: false
Store: unistore
```

Contributor guide

Open the contributing guide

Research direction

Start by running the supplied setup and both SELECT and EXPLAIN queries on TiDB v8.5.7, then compare the optimizer handling of the plain predicate with the double-negated form. Done means the equivalent queries receive comparable plans, with the LEFT JOIN right side pruned in both cases, and regression coverage verifies the behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, sql
Domain
databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.