Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/executor/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,8 @@ func ResetContextOfStmt(ctx sessionctx.Context, s ast.StmtNode) (err error) {
vars.StmtCtx = sc
vars.PrevFoundInPlanCache = vars.FoundInPlanCache
vars.FoundInPlanCache = false
vars.PrevFoundInTrivialPlan = vars.FoundInTrivialPlan
vars.FoundInTrivialPlan = false
vars.PrevFoundInBinding = vars.FoundInBinding
vars.FoundInBinding = false
vars.DurationWaitTS = 0
Expand Down
1 change: 1 addition & 0 deletions pkg/planner/core/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ go_library(
"task.go",
"telemetry.go",
"trace.go",
"trivial_plan.go",
"util.go",
],
importpath = "github.com/pingcap/tidb/pkg/planner/core",
Expand Down
3 changes: 2 additions & 1 deletion pkg/planner/core/tests/pointget/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ go_test(
srcs = [
"main_test.go",
"point_get_plan_test.go",
"trivial_plan_test.go",
],
flaky = True,
shard_count = 8,
shard_count = 10,
deps = [
"//pkg/config/kerneltype",
"//pkg/metrics",
Expand Down
157 changes: 157 additions & 0 deletions pkg/planner/core/tests/pointget/trivial_plan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2026 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pointget

import (
"testing"

"github.com/pingcap/tidb/pkg/testkit"
"github.com/stretchr/testify/require"
)

// requireTrivialPlan asserts that the most recently executed statement did (or
// did not) use the trivial plan fast path, by checking @@last_plan_from_trivial.
func requireTrivialPlan(t *testing.T, tk *testkit.TestKit, expected bool) {
t.Helper()
val := "0"
if expected {
val = "1"
}
tk.MustQuery("select @@last_plan_from_trivial").Check(testkit.Rows(val))
}

func TestTrivialPlan(t *testing.T) {
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.MustExec("use test")

// Table with no secondary indexes — eligible for trivial plan.
tk.MustExec("drop table if exists t_simple")
tk.MustExec("create table t_simple(a int primary key, b int, c varchar(32))")
tk.MustExec("insert into t_simple values(1, 10, 'hello'), (2, 20, 'world'), (3, 30, 'foo')")

// Basic full table scan: SELECT *
rows := tk.MustQuery("select * from t_simple").Sort().Rows()
require.Len(t, rows, 3)
require.Equal(t, "1", rows[0][0])
require.Equal(t, "hello", rows[0][2])
requireTrivialPlan(t, tk, true)

// SELECT with specific columns.
rows = tk.MustQuery("select a, c from t_simple").Sort().Rows()
require.Len(t, rows, 3)
require.Equal(t, "foo", rows[2][1])
requireTrivialPlan(t, tk, true)

// SELECT with column alias.
rows = tk.MustQuery("select a as id, b as val from t_simple").Sort().Rows()
require.Len(t, rows, 3)
requireTrivialPlan(t, tk, true)

// SELECT with reordered columns (verifies column mapping correctness).
rows = tk.MustQuery("select c, a from t_simple").Sort().Rows()
require.Len(t, rows, 3)
// Sorted by first column (c): "foo"→3, "hello"→1, "world"→2
require.Equal(t, "foo", rows[0][0])
require.Equal(t, "3", rows[0][1])
require.Equal(t, "hello", rows[1][0])
require.Equal(t, "1", rows[1][1])
requireTrivialPlan(t, tk, true)

// SELECT with duplicate column references.
rows = tk.MustQuery("select a, a from t_simple").Sort().Rows()
require.Len(t, rows, 3)
require.Equal(t, rows[0][0], rows[0][1])
requireTrivialPlan(t, tk, true)

// Verify EXPLAIN shows a table scan plan (EXPLAIN uses normal optimizer).
tk.MustQuery("explain format = 'brief' select * from t_simple").Check(testkit.Rows(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Tests don't prove the fast path is used

Why: TestTrivialPlan validates EXPLAIN output, but TryTrivialPlan explicitly returns nil in EXPLAIN mode (pkg/planner/core/trivial_plan.go:55), so the test can pass even if the fast path never triggers for actual queries.

Evidence: pkg/planner/core/trivial_plan.go:55:

if ctx.GetSessionVars().StmtCtx.InExplainStmt {
    return nil, nil
}

The test at pkg/planner/core/tests/pointget/trivial_plan_test.go:50 checks explain format='brief' select * from t_simple, which will always use the normal optimizer path, not the trivial fast path.

"TableReader 10000.00 root data:TableFullScan",
"└─TableFullScan 10000.00 cop[tikv] table:t_simple keep order:false, stats:pseudo",
))
}

func TestTrivialPlanFallback(t *testing.T) {
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.MustExec("use test")
tk.MustExec("drop table if exists t_idx, t_part, t_gen")

// Table with a secondary index — should NOT use trivial plan (optimizer
// needs to decide between table scan and index scan).
tk.MustExec("create table t_idx(a int primary key, b int, index idx_b(b))")
tk.MustExec("insert into t_idx values(1, 10), (2, 20)")
rows := tk.MustQuery("select * from t_idx").Sort().Rows()
require.Len(t, rows, 2)
requireTrivialPlan(t, tk, false)

// Partitioned table — should NOT use trivial plan.
tk.MustExec("create table t_part(a int primary key, b int) partition by hash(a) partitions 4")
tk.MustExec("insert into t_part values(1, 10), (2, 20)")
rows = tk.MustQuery("select * from t_part").Sort().Rows()
require.Len(t, rows, 2)
requireTrivialPlan(t, tk, false)

// Table with virtual generated column — should NOT use trivial plan.
tk.MustExec("create table t_gen(a int primary key, b int, c int as (a + b))")
tk.MustExec("insert into t_gen(a, b) values(1, 10), (2, 20)")
rows = tk.MustQuery("select * from t_gen").Sort().Rows()
require.Len(t, rows, 2)
requireTrivialPlan(t, tk, false)

// Query with WHERE clause on a trivial table — should NOT use trivial plan.
tk.MustExec("drop table if exists t_no_idx")
tk.MustExec("create table t_no_idx(a int primary key, b int)")
tk.MustExec("insert into t_no_idx values(1, 10), (2, 20)")
rows = tk.MustQuery("select * from t_no_idx where b > 5").Rows()
require.Len(t, rows, 2)
requireTrivialPlan(t, tk, false)

// Query with ORDER BY — should NOT use trivial plan.
rows = tk.MustQuery("select * from t_no_idx order by b").Rows()
require.Len(t, rows, 2)
requireTrivialPlan(t, tk, false)

// Query with LIMIT — should NOT use trivial plan.
rows = tk.MustQuery("select * from t_no_idx limit 1").Rows()
require.Len(t, rows, 1)
requireTrivialPlan(t, tk, false)

// Query with DISTINCT — should NOT use trivial plan.
rows = tk.MustQuery("select distinct b from t_no_idx").Rows()
require.Len(t, rows, 2)
requireTrivialPlan(t, tk, false)

// Query with expression in SELECT — should NOT use trivial plan
// (buildSchemaFromFields returns nil for non-column expressions).
rows = tk.MustQuery("select a + 1 from t_no_idx").Rows()
require.Len(t, rows, 2)
requireTrivialPlan(t, tk, false)

// sql_select_limit — should NOT use trivial plan.
tk.MustExec("set @@sql_select_limit = 1")
rows = tk.MustQuery("select * from t_no_idx").Rows()
require.Len(t, rows, 1)
requireTrivialPlan(t, tk, false)
tk.MustExec("set @@sql_select_limit = default")

// Dirty transaction (uncommitted writes) — should NOT use trivial plan.
tk.MustExec("begin")
tk.MustExec("insert into t_no_idx values(3, 30)")
rows = tk.MustQuery("select * from t_no_idx").Sort().Rows()
require.Len(t, rows, 3)
requireTrivialPlan(t, tk, false)
tk.MustExec("rollback")
}
Loading
Loading