Redundant sort in the query plan of normal SELECT, causes it slower than the prepared SELECT
- 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)
Hi,
In the following test case, there are two equivalent queries, one is a normal SELECT, the other is a prepared SELECT. However, I found that the normal SELECT slower than the prepared SELECT. After analyzing the query plan, I found there is a sort operation in the query plan of normal SELECT, but not in the prepared SELECT. Given that prepared statements typically result in suboptimal query plans, the query plan generation process for normal SELECT statements may still have room for further optimization.
```
CREATE TABLE t0(c0 BOOL);
INSERT INTO t0 (c0)
WITH RECURSIVE seq AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM seq WHERE n < 1000
)
SELECT FLOOR(RAND() * 2) FROM seq;
ANALYZE TABLE database39o.t0;
EXPLAIN ANALYZE SELECT t0.c0 FROM t0 WHERE true GROUP BY t0.c0 HAVING t0.c0 ORDER BY '+\\\'\n*\'#', t0.c0 DESC, t0.c0;
id estRows actRows task access object execution info operator info memory disk
Sort_8 640.00 1 root time:1.96ms, open:143.6µs, close:22µs, loops:2, RU:0.81 database39o.t0.c0:desc 396 Bytes 0 Bytes
└─Selection_12 640.00 1 root time:1.83ms, open:127.7µs, close:18.2µs, loops:2 database39o.t0.c0 380 Bytes N/A
└─HashAgg_21 800.00 2 root time:1.79ms, open:124µs, close:16.8µs, loops:3, partial_worker:{wall_time:1.561265ms, concurrency:5, task_num:1, tot_wait:1.436884ms, tot_exec:25.371µs, tot_time:7.44519ms, max:1.496044ms, p95:1.496044ms}, final_worker:{wall_time:1.661123ms, concurrency:5, task_num:5, tot_wait:11.269µs, tot_exec:189ns, tot_time:7.63241ms, max:1.569574ms, p95:1.569574ms} group by:database39o.t0.c0, funcs:firstrow(database39o.t0.c0)->database39o.t0.c0 13.5 KB 0 Bytes
└─TableReader_22 800.00 2 root time:1.52ms, open:66.4µs, close:11µs, loops:2, cop_task: {num: 1, max: 1.35ms, proc_keys: 0, tot_proc: 1ms, copr_cache_hit_ratio: 0.00, build_task_duration: 24.5µs, max_distsql_concurrency: 1}, fetch_resp_duration: 1.41ms, rpc_info:{Cop:{num_rpc:1, total_time:1.31ms}} data:HashAgg_14 225 Bytes N/A
└─HashAgg_14 800.00 2 cop[tikv] tikv_task:{time:1.22ms, loops:0}, time_detail: {total_process_time: 1ms} group by:database39o.t0.c0, N/A N/A
└─TableFullScan_20 1000.00 1000 cop[tikv] table:t0 tikv_task:{time:1.22ms, loops:0} keep order:false, stats:pseudo N/A N/A
SET @a = true;
PREPARE explain_prepare_query FROM "EXPLAIN ANALYZE SELECT t0.c0 FROM t0 WHERE ? GROUP BY t0.c0 HAVING t0.c0 ORDER BY '+\\\'\n*\'#', t0.c0 DESC, t0.c0;";
EXECUTE explain_prepare_query USING @a;
id estRows actRows task access object execution info operator info memory disk
Selection_12 640.00 1 root time:1.48ms, open:100.7µs, close:11.1µs, loops:2, RU:0.81 database39o.t0.c0 380 Bytes N/A
└─HashAgg_21 800.00 2 root time:1.47ms, open:98.3µs, close:10.7µs, loops:3, partial_worker:{wall_time:1.311444ms, concurrency:5, task_num:1, tot_wait:1.202566ms, tot_exec:17.659µs, tot_time:6.158581ms, max:1.236934ms, p95:1.236934ms}, final_worker:{wall_time:1.339565ms, concurrency:5, task_num:5, tot_wait:5.903µs, tot_exec:242ns, tot_time:6.276409ms, max:1.265245ms, p95:1.265245ms} group by:database39o.t0.c0, funcs:firstrow(database39o.t0.c0)->database39o.t0.c0 13.5 KB 0 Bytes
└─TableReader_22 800.00 2 root time:1.29ms, open:53.5µs, close:6.93µs, loops:2, cop_task: {num: 1, max: 1.15ms, proc_keys: 0, tot_proc: 1ms, copr_cache_hit_ratio: 0.00, build_task_duration: 15.9µs, max_distsql_concurrency: 1}, fetch_resp_duration: 1.2ms, rpc_info:{Cop:{num_rpc:1, total_time:1.12ms}} data:HashAgg_14 225 Bytes N/A
└─HashAgg_14 800.00 2 cop[tikv] tikv_task:{time:1.05ms, loops:0}, time_detail: {total_process_time: 1ms} group by:database39o.t0.c0, N/A N/A
└─TableFullScan_20 1000.00 1000 cop[tikv] table:t0 tikv_task:{time:1.05ms, loops:0} keep order:false, stats:pseudo N/A N/A
```
### 2. What did you expect to see? (Required)
The normal SELECT should be faster or the same with the prepared SELECT.
### 3. What did you see instead (Required)
The normal SELECT is slower than the prepared SELECT.
### 4. What is your TiDB version? (Required)
Release Version: v9.0.0-beta.2.pre-1118-gfc6f7b2e42\nEdition: Community\nGit Commit Hash: https://github.com/pingcap/tidb/commit/fc6f7b2e42be08957a9b80e5761bfa4793066353\nGit Branch: master\nUTC Build Time: 2026-01-26 12:27:51\nGoVersion: go1.25.6\nRace Enabled: false\nCheck Table Before Drop: false\nStore: unistore\nKernel Type: Classic
Contributor guide
Research direction
Start by reproducing the normal and prepared SELECT statements with EXPLAIN ANALYZE and comparing their query plans, as shown in the issue. Trace the planner path that produces Sort_8 for the normal query; done means equivalent plans without the redundant sort, with regression coverage for this case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, sql
- Domain
- databases, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100