ORDER BY pruneByItems can drop a hash-colliding CAST sort key and change SELECT/DELETE/UPDATE ORDER BY LIMIT targets
- 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)
Run against a local TiDB master:
```sql
CREATE TABLE t_order (id INT PRIMARY KEY, dt DATETIME);
INSERT INTO t_order VALUES
(1, '2024-01-15 10:00:00'),
(2, '2024-01-15 09:00:00'),
(3, '2024-01-16 08:00:00');
-- Truth: second key is the base DATETIME column, so no collision.
SELECT GROUP_CONCAT(id ORDER BY CAST(dt AS DATE), dt, id) FROM t_order; -- 2,1,3
-- RED: second key is CAST(dt AS DATETIME), which hash-collides with the first.
SELECT GROUP_CONCAT(id ORDER BY CAST(dt AS DATE), CAST(dt AS DATETIME), id)
FROM t_order; -- 1,2,3
```
`EXPLAIN` isolates the path:
```text
control: Sort_... Column#5, t_order.dt, t_order.id
RED: Sort_... Column#5, t_order.id
```
The DML consequence is the same helper on `DELETE` / `UPDATE ... ORDER BY ... LIMIT`:
| Statement | Remaining / marked | Correct truth |
| --- | --- | --- |
| `DELETE ... ORDER BY CAST(dt AS DATE), CAST(dt AS DATETIME), id LIMIT 1` | leaves `2,3` (deletes id=1) | leaves `1,3` (deletes id=2) |
| `DELETE ... ORDER BY CAST(dt AS DATE), dt, id LIMIT 1` (control) | leaves `1,3` | `1,3` |
| `UPDATE ... ORDER BY CAST(dt AS DATE), CAST(dt AS DATETIME), id LIMIT 1 SET marker=1` | marks id=1 | marks id=2 |
| `UPDATE ... ORDER BY CAST(dt AS DATE), dt, id LIMIT 1 SET marker=1` (control) | marks id=2 | id=2 |
The first key plus the remaining unique tie-breaker makes the intended order deterministic; the wrong result is not SQL-level "unspecified order".
### 2. What did you expect to see? (Required)
`CAST(dt AS DATE)` and `CAST(dt AS DATETIME)` are semantically distinct: one discards time-of-day, the other preserves it. Their ordinary `Expression.HashCode()` bytes are identical because `ScalarFunction.ReHashCode` appends only the broad return `EvalType`, and both targets use `ETDatetime`.
`pruneByItems` uses `string(byItem.Expr.HashCode())` as the `seen`-set key for `ORDER BY` expressions. When the two CASTs are adjacent sort keys, the second is treated as a duplicate and removed. The second key is not projected, not sorted by, and not evaluated. Without `LIMIT` the row set is unchanged but the order is wrong; with `LIMIT`, `DELETE`, or `UPDATE`, the wrong row is returned or modified.
This is a new consumer root in the CAST target-type hash family. It is a sibling, not a duplicate, of id4230003 (`RemoveDupExprs` predicate path): a fix local to `RemoveDupExprs` does not close `pruneByItems`.
### 3. What did you see instead (Required)
colliding GROUP_CONCAT=1,2,3 vs truth=2,1,3; colliding plan Sort=Column#5, t_order.id vs control Sort=Column#5, t_order.dt, t_order.id; colliding DELETE LIMIT 1 left 2,3 vs control left 1,3; colliding UPDATE LIMIT 1 marked id=1 vs control marked id=2.
### 4. What is your TiDB version? (Required)
```text
Release Version: v8.4.0-this-is-a-placeholder
Edition: Community
Git Commit Hash: None
Git Branch: None
UTC Build Time: None
GoVersion: go1.25.12
Race Enabled: false
Check Table Before Drop: false
Store: unistore
Kernel Type: Classic
```
Built from source commit `a514a92784c9654502686e6ee6efc9e0aeda8afa` (pingcap/tidb master, 2026-09-07).
### 5. Root cause (optional)
- pkg/planner/core/operator/logicalop/logical_plans_misc.go:139-167 pruneByItems seen map keyed by string(byItem.Expr.HashCode())
- pkg/planner/core/operator/logicalop/logical_sort.go:69 LogicalSort.PruneColumns calls pruneByItems
- pkg/planner/core/operator/logicalop/logical_top_n.go:87 LogicalTopN.PruneColumns calls pruneByItems
- pkg/planner/core/operator/logicalop/logical_aggregation.go:139 LogicalAggregation.PruneColumns calls pruneByItems for aggregate ORDER BY
- pkg/expression/scalar_function.go:765-791 ReHashCode appends only EvalType for CAST
Contributor guide
Research direction
Start at pruneByItems in pkg/planner/core/operator/logicalop/logical_plans_misc.go and trace its callers in logical_sort.go, logical_top_n.go, and logical_aggregation.go. Inspect ScalarFunction.ReHashCode in pkg/expression/scalar_function.go, then run the reported SELECT, DELETE, and UPDATE reproductions and compare their plans and results. Done means the distinct CAST sort keys remain present and the reported outcomes match the stated truth.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, sql
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100