-
Notifications
You must be signed in to change notification settings - Fork 6.2k
planner: add trivial plan fast path for simple full-table scans #67376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
terry1purcell
wants to merge
11
commits into
pingcap:master
Choose a base branch
from
terry1purcell:streamline2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
7cf9d74
planner: add trivial plan fast path for simple full-table scans
terry1purcell d513a54
chore: update bazel file
terry1purcell dd47425
planner: skip trivial plan fast path for views and sequences
terry1purcell cfe8b02
planner: skip trivial plan for tables with FKs or hidden columns
terry1purcell e4648f4
Review comments
terry1purcell 7c02742
Review comments2
terry1purcell 069ba47
planner: broaden trivial plan gate to allow WHERE and indexed tables
terry1purcell 71da7a0
planner: bail trivial plan when WHERE refs need stats loading
terry1purcell b023304
planner: mirror predicate-column tracking in trivial plan
terry1purcell f175c68
planner: opt binding plan generation out of optimizer fast paths
terry1purcell dc8a3ca
planner: cast string-typed trivial WHERE predicates to numeric
terry1purcell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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( | ||
| "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") | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
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.