pingcap / pingcap/tidb

read-only user variable is corrupted after SELECT/EXPLAIN when getvar() stays in root plan

Open
#67,904 2 comments 0 reactions 1 assignee Claimed by @qw4990 View on GitHub
affects-8.5 contribution report/customer 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!

### 1. Minimal reproduce step (Required)

Reproduced locally on **2026-04-20** against a local `tiup playground v8.5.6` cluster (`127.0.0.1:4000`).

```sql
DROP TABLE IF EXISTS statement_bug;
DROP TABLE IF EXISTS folio_bug;
DROP TABLE IF EXISTS customer_bug;

CREATE TABLE `customer_bug` (
`Id` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`PortfolioCode` varchar(255) COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `folio_bug` (
`Id` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`CustomerId` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`Type` varchar(50) COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`Number` int DEFAULT NULL,
PRIMARY KEY (`Id`),
KEY `IX_CustomerId` (`CustomerId`),
CONSTRAINT `FK_Folio_Customer` FOREIGN KEY (`CustomerId`) REFERENCES `customer_bug` (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_0900_ai_ci;

CREATE TABLE `statement_bug` (
`Id` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`CustomerId` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`FolioId` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`Number` int NOT NULL,
`CreatedOn` datetime NOT NULL,
`JournalNumberFrom` int NOT NULL,
`JournalNumberTo` int NOT NULL,
`FolioCode` varchar(255) COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`Status` text COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`AccountDetails` text COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`StatementDate` datetime DEFAULT NULL,
`DocumentStorageId` char(36) COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`RentAndTenancyText` text COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`OpeningBalance` decimal(12,2) DEFAULT NULL,
`ClosingBalance` decimal(12,2) DEFAULT NULL,
`TrustAccountType` int DEFAULT NULL,
PRIMARY KEY (`Id`),
KEY `FK_Statement_Customer` (`CustomerId`),
KEY `FK_Statement_Folio` (`FolioId`),
CONSTRAINT `FK_Statement_Customer` FOREIGN KEY (`CustomerId`) REFERENCES `customer_bug` (`Id`) ON DELETE CASCADE,
CONSTRAINT `FK_Statement_Folio` FOREIGN KEY (`FolioId`) REFERENCES `folio_bug` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_0900_ai_ci;

INSERT INTO customer_bug VALUES ('11111111-1111-1111-1111-111111111111', 'SUPER6DO');

INSERT INTO folio_bug VALUES (
'22222222-2222-2222-2222-222222222222',
'11111111-1111-1111-1111-111111111111',
'supplier', 1
);

INSERT INTO statement_bug VALUES (
UUID(), '11111111-1111-1111-1111-111111111111',
'22222222-2222-2222-2222-222222222222',
1, NOW(), 1, 10, 'FOLIO1', 'Active', 'Account details here',
NOW(), UUID(), 'Rent text here', 100.00, 200.00, 1
);
INSERT INTO statement_bug VALUES (
UUID(), '11111111-1111-1111-1111-111111111111',
'22222222-2222-2222-2222-222222222222',
2, NOW(), 11, 20, 'FOLIO1', 'Closed', 'More account details',
NOW(), UUID(), 'More rent text', 300.00, 400.00, 1
);

INSERT INTO customer_bug VALUES ('33333333-3333-3333-3333-333333333333', 'OTHER');
INSERT INTO folio_bug VALUES (
'44444444-4444-4444-4444-444444444444',
'33333333-3333-3333-3333-333333333333',
'other', 1
);

INSERT INTO statement_bug
SELECT
UUID(), '33333333-3333-3333-3333-333333333333',
'44444444-4444-4444-4444-444444444444',
seq, NOW(), seq, seq + 10, CONCAT('CODE', seq),
CONCAT('BulkStatus', seq), CONCAT('BulkAccount', seq),
NOW(), UUID(), CONCAT('BulkRent', seq),
ROUND(RAND() * 1000, 2), ROUND(RAND() * 1000, 2), 1
FROM (
WITH RECURSIVE nums AS (
SELECT 3 AS seq
UNION ALL
SELECT seq + 1 FROM nums WHERE seq < 1001
)
SELECT seq FROM nums
) AS gen;

SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci;

SET @CustomerId = (SELECT Id FROM customer_bug WHERE PortfolioCode = 'SUPER6DO');
SET @FolioId = (SELECT Id FROM folio_bug WHERE CustomerId = @CustomerId AND Type = 'supplier' AND Number = 1);

SELECT @FolioId;
-- expected: 22222222-2222-2222-2222-222222222222

EXPLAIN SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;
SELECT @FolioId;
-- on my v8.5.6 playground this is already corrupted to:
-- test.statement_bug.numbereq(test.sta

SET @FolioId = (SELECT Id FROM folio_bug WHERE CustomerId = @CustomerId AND Type = 'supplier' AND Number = 1);
SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;
SELECT @FolioId;
-- expected: 22222222-2222-2222-2222-222222222222
-- actual on my v8.5.6 playground:
-- ActiveClosed2-2222-2222-222222222222
```

The `EXPLAIN` plan on my playground is:

```sql
+---------------------+----------+-----------+---------------------+------------------------------------------------+
| id | estRows | task | access object | operator info |
+---------------------+----------+-----------+---------------------+------------------------------------------------+
| Sort_5 | 8000.00 | root | | statement_bug.Number |
| └─Selection_8 | 8000.00 | root | | eq(statement_bug.FolioId, getvar("folioid")) |
| └─TableReader_10 | 10000.00 | root | | data:TableFullScan_9 |
| └─TableFullScan_9 | 10000.00 | cop[tikv] | table:statement_bug | keep order:false, stats:pseudo |
+---------------------+----------+-----------+---------------------+------------------------------------------------+
```

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

`@FolioId` is only read here, never assigned inside the `SELECT` or `EXPLAIN`, so its value should stay unchanged:

```sql
22222222-2222-2222-2222-222222222222
```

This should remain true both after:

```sql
EXPLAIN SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;
```

and after:

```sql
SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;
```

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

I can reproduce two forms of corruption on the same v8.5.6 playground:

1. Running `EXPLAIN SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;` mutates `@FolioId` from the UUID to:

```sql
test.statement_bug.numbereq(test.sta
```

After that, the following `SELECT ... WHERE FolioId = @FolioId` returns `0` rows because the variable is already corrupted.

2. If I reset `@FolioId` and run the `SELECT` directly without the preceding `EXPLAIN`, the query returns the expected 2 matching rows, but `@FolioId` is still corrupted afterwards:

```sql
ActiveClosed2-2222-2222-222222222222
```

This looks like the variable buffer is being overwritten by data from the result / plan text, even though the query only reads `@FolioId`.

I searched for existing issues and found related-but-different reports such as:

- #36944 `wrong results when the query contains setting user var`
- #66339 `planner: read-only variables with uppercase name cannot be converted to constant and lead to sub-optimal plans`
- #52742 `Optimizer should use index when comparing with user-defined variables`

but I could not find one describing the user variable value itself being overwritten by `SELECT` / `EXPLAIN` output like this.

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

```sql
SELECT tidb_version();

Release Version: v8.5.6
Edition: Community
Git Commit Hash: ae18096e023780bb56bfce33698abec0d4640d0a
Git Branch: HEAD
UTC Build Time: 2026-04-14 07:11:54
GoVersion: go1.25.8
Race Enabled: false
Check Table Before Drop: false
Store: tikv
```

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.