pingcap / pingcap/tidb

planner: SELECT ... FOR UPDATE point/batch-point-get may incorrectly choose TiFlash under tidb_enforce_mpp=1

Open
#67,485 2 comments 0 reactions 1 assignee Claimed by @qw4990 View on GitHub
may-affects-7.1 may-affects-7.5 may-affects-8.1 may-affects-8.5 severity/critical 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!

### 1. Minimal reproduce step (Required)

For SELECT ... FOR UPDATE in an explicit transaction, point-get / batch-point-get should not use TiFlash (MPP/Cop), because locking reads must be handled by TiKV path. Even with tidb_enforce_mpp=1, planner should gracefully keep Point_Get / Batch_Point_Get (non-TiFlash path), instead of planning TiFlash for locking reads.

TiKV will add a lock for the predicate during processing point get even if there is no data.
However, only the existing data can add lock when using coprocessor in the current implementation.
So the plan of using point get is not equal to the plan of using coprocessor in the presence of for update.

This looks like a regression introduced by:

- commit 1776433ee098d699de8f315c6c9b6434b551ad1e
- PR: #65127 (planner: fix no access path when TiKV read is disabled under RC isolation)

Likely root cause:

- IsForUpdateRead is overloaded (both RC current read and SELECT ... FOR UPDATE).
- In find_best_task, the point-get TiFlash filtering branch became conditional on isolationReadEnginesHasTiKV.
- When TiKV is not in tidb_isolation_read_engines, that branch is skipped, so TiFlash point-get path can survive under tidb_enforce_mpp=1.

A small inconsistent case
```sql
CREATE TABLE lock_key (
id BIGINT PRIMARY KEY,
v INT
);
CREATE TABLE side_effect (
worker INT PRIMARY KEY,
cnt INT NOT NULL
);

INSERT INTO side_effect VALUES (1, 0), (2, 0);

ALTER TABLE lock_key SET TIFLASH REPLICA 1;
-- Wait until TiFlash replica is available:
-- SELECT * FROM information_schema.tiflash_replica
-- WHERE TABLE_SCHEMA='test' AND TABLE_NAME='lock_key';

-- Use two sessions (A and B) with the same settings:

SET @@tidb_txn_mode='pessimistic';
SET @@transaction_isolation='REPEATABLE-READ';
SET @@tidb_allow_mpp=1;
SET @@tidb_enforce_mpp=1;
SET @@tidb_isolation_read_engines='tidb,tiflash';

-- Session A

BEGIN;

EXPLAIN SELECT * FROM lock_key WHERE id = 2 FOR UPDATE;
+--------------------------+---------+--------------+----------------+---------------------------------------------+
| id | estRows | task | access object | operator info |
+--------------------------+---------+--------------+----------------+---------------------------------------------+
| SelectLock | 1.00 | root | | for update 0 |
| └─TableReader | 1.00 | root | | MppVersion: 3, data:ExchangeSender |
| └─ExchangeSender | 1.00 | mpp[tiflash] | | ExchangeType: PassThrough |
| └─TableRangeScan | 1.00 | mpp[tiflash] | table:lock_key | range:[2,2], keep order:false, stats:pseudo |
+--------------------------+---------+--------------+----------------+---------------------------------------------+

SELECT * FROM lock_key WHERE id = 2 FOR UPDATE;
UPDATE side_effect
SET cnt = cnt + 1
WHERE worker = 1
AND NOT EXISTS (SELECT 1 FROM lock_key WHERE id = 2);

SELECT SLEEP(8);
INSERT IGNORE INTO lock_key VALUES (2);
COMMIT;

#### Session B (run while Session A is sleep)

BEGIN;
SELECT * FROM lock_key WHERE id = 2 FOR UPDATE; -- should block, but not actually

UPDATE side_effect
SET cnt = cnt + 1
WHERE worker = 2
AND NOT EXISTS (SELECT 1 FROM lock_key WHERE id = 2);
INSERT IGNORE INTO lock_key VALUES (2);
COMMIT;
```

- Expected (TiKV point-get with real locking):
- Session B blocks on SELECT ... FOR UPDATE until Session A commits.
- total_cnt = 1.
- Unexpected (mistakenly routed to TiFlash, no lock):
- Session B does not block on SELECT ... FOR UPDATE.
- Both A and B pass NOT EXISTS, so total_cnt = 2 (data inconsistency).

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

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

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

Release Version: v9.0.0-beta.2.pre-1496-gfbce36ca94
Edition: Community
Git Commit Hash: fbce36ca9449b2d96059a5fabcddf5866b6fa6d7
Git Branch: HEAD
UTC Build Time: 2026-03-31 06:35:28
GoVersion: go1.25.8
Race Enabled: false
Check Table Before Drop: false
Store: tikv
Kernel Type: Classic

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.