Skip to content

planner: add trivial plan fast path for simple full-table scans#67376

Open
terry1purcell wants to merge 11 commits into
pingcap:masterfrom
terry1purcell:streamline2
Open

planner: add trivial plan fast path for simple full-table scans#67376
terry1purcell wants to merge 11 commits into
pingcap:masterfrom
terry1purcell:streamline2

Conversation

@terry1purcell
Copy link
Copy Markdown
Contributor

@terry1purcell terry1purcell commented Mar 27, 2026

For queries like "SELECT * FROM t" on tables with no secondary indexes, no TiFlash replicas, no predicates, and no complex clauses, the plan is predetermined as a full table scan. This adds a fast path that constructs the PhysicalTableReader → PhysicalTableScan directly, skipping all 29 logical optimization rules, synchronous stats loading, cost estimation, and post-optimization passes.

The fast path is skipped during EXPLAIN so the normal optimizer produces full plan metadata for diagnostic output.

What problem does this PR solve?

Issue Number: ref #67351

Problem Summary:

What changed and how does it work?

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No need to test
    • I checked and no code files have been changed.

Side effects

  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Breaking backward compatibility

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

Please refer to Release Notes Language Style Guide to write a quality release note.

None

Summary by CodeRabbit

  • New Features

    • Adds a trivial fast-path planner to return a direct physical plan for very simple single-table SELECTs to speed up planning when applicable.
    • Exposes a read-only session-visible flag "last_plan_from_trivial" and session tracking to indicate whether the previous statement used the trivial fast path.
  • Tests

    • Adds comprehensive tests validating trivial-plan usage and correct fallbacks for queries and table shapes that disqualify the fast path.

Review Change Stack

For queries like "SELECT * FROM t" on tables with no secondary indexes,
no TiFlash replicas, no predicates, and no complex clauses, the plan is
predetermined as a full table scan. This adds a fast path that constructs
the PhysicalTableReader → PhysicalTableScan directly, skipping all 29
logical optimization rules, synchronous stats loading, cost estimation,
and post-optimization passes.

The fast path is skipped during EXPLAIN so the normal optimizer produces
full plan metadata for diagnostic output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ti-chi-bot ti-chi-bot Bot added the release-note-none Denotes a PR that doesn't merit a release note. label Mar 27, 2026
@pantheon-ai
Copy link
Copy Markdown

pantheon-ai Bot commented Mar 27, 2026

Review Complete

Findings: 6 issues
Posted: 5
Duplicates/Skipped: 1

ℹ️ Learn more details on Pantheon AI.

@ti-chi-bot ti-chi-bot Bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. sig/planner SIG: Planner labels Mar 27, 2026
@tiprow
Copy link
Copy Markdown

tiprow Bot commented Mar 27, 2026

Hi @terry1purcell. Thanks for your PR.

PRs from untrusted users cannot be marked as trusted with /ok-to-test in this repo meaning untrusted PR authors can never trigger tests themselves. Collaborators can still trigger tests on the PR using /test all.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a trivial-plan fast-path to the planner that can directly produce a PhysicalTableReader for constrained single-table SELECTs, integrates it into optimizeNoCache with binding checks and early-exit, adds session flags and a read-only sysvar to track usage, and includes tests and build updates.

Changes

Cohort / File(s) Summary
Build / Tests
pkg/planner/core/BUILD.bazel, pkg/planner/core/tests/pointget/BUILD.bazel, pkg/planner/core/tests/pointget/trivial_plan_test.go
Add trivial_plan.go to core sources, add trivial_plan_test.go to pointget tests, increase test shard_count 8→12; introduce TestTrivialPlan, TestTrivialPlanFallback, and related tests and helper.
Core Planner
pkg/planner/core/trivial_plan.go
New TryTrivialPlan(ctx, node) implementing the trivial SELECT fast-path with eligibility guards, schema & scan-column construction, WHERE rewrite and validation, conservative index gating, row-count estimation (realtime/pseudo), pseudo histogram, range computation, builds PhysicalTableScan (+ optional PhysicalSelection/PhysicalProjection) and PhysicalTableReader, and records accessed tables; includes isTrivialSelect, isTrivialTable, mayUseIndex, indexCoversCols, whereColumnVisitor, and trivialRowCountEstimate.
Optimizer Integration
pkg/planner/optimize.go
Invoke core.TryTrivialPlan in optimizeNoCache (TiKV path) after other fast-path checks; if a trivial plan is returned and no SQL binding matches, run core.CheckTableMode(node), set sessVars.FoundInTrivialPlan = true, and return the trivial plan early; otherwise continue full optimization.
Executor / Session State & Sysvar
pkg/executor/select.go, pkg/sessionctx/vardef/tidb_vars.go, pkg/sessionctx/variable/session.go, pkg/sessionctx/variable/sysvar.go
Preserve/reset FoundInTrivialPlan across statements, add TiDBFoundInTrivialPlan (last_plan_from_trivial) constant and read-only sysvar exposing previous-statement value, and add FoundInTrivialPlan/PrevFoundInTrivialPlan SessionVars fields.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Optimizer as "optimizeNoCache\n(Optimizer)"
    participant Core as "TryTrivialPlan\n(Core)"
    participant Stats as "Stats Cache / Realtime"
    participant Scan as "PhysicalTableScan\n(Scan)"
    participant Reader as "PhysicalTableReader\n(Reader)"

    Client->>Optimizer: Submit SELECT
    Optimizer->>Core: TryTrivialPlan(ctx, node)
    alt Trivial conditions met and no binding match
        Core->>Core: Validate SELECT shape & table
        Core->>Stats: Read cached realtime / pseudo counts
        Stats-->>Core: Row estimate & hist placeholder
        Core->>Scan: Build PhysicalTableScan (pruned cols, ranges)
        Scan-->>Reader: Wrap into PhysicalTableReader (apply stats)
        Reader-->>Optimizer: Return physical plan
        Optimizer-->>Client: Execute returned plan
    else Not trivial or binding matched
        Core-->>Optimizer: Return nil
        Optimizer->>Optimizer: Continue full optimization pipeline
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

size/L, ok-to-test

Suggested reviewers

  • RidRisR
  • YuJuncen

Poem

🐇 I sniffed the planner, tidy and small,
A single-table shortcut, straight as a wall,
Build columns, check stats, no indexes to spar,
Scan, wrap, and return — a hop, not far,
Carrots for speed, a quick little call.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main feature: adding a fast-path planner optimization for trivial full-table scans, which is the primary change across all modified files.
Description check ✅ Passed The PR description provides the issue reference (ref #67351), explains the problem and solution, includes unit tests, and documents that no breaking changes or documentation updates are needed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

terry1purcell and others added 2 commits March 27, 2026 18:31
Views need the full optimizer to expand their definition into the
underlying query. Without this check, SELECT * FROM a_view would
produce an empty result because the fast path constructs a table
scan against the view's metadata rather than its underlying tables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/planner/core/tests/pointget/trivial_plan_test.go (1)

56-104: Consider adding direct verification that trivial plan path is (not) taken.

The fallback tests verify correct query results but don't directly assert that the trivial plan fast path was skipped. While the correctness tests are valuable, consider adding a mechanism (e.g., a metric counter or session variable) to directly verify the optimization path taken. This would make the tests more robust against future regressions where the trivial path might incorrectly activate.

That said, the current tests adequately verify end-to-end correctness, which is the primary goal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/tests/pointget/trivial_plan_test.go` around lines 56 - 104,
Add an assertion in TestTrivialPlanFallback to directly verify the trivial-plan
fast path was not used by checking a test-only observable (e.g., a planner
metric counter or session variable). Instrument the planner (e.g., increment a
planner.TrivialPlanCounter or set a boolean flag when trivial path is taken) and
in the test read that observable before/after each query in
TestTrivialPlanFallback to assert it did not increase (or the flag is false);
reference TestTrivialPlanFallback and the planner observable (e.g.,
TrivialPlanCounter / trivialPathTaken) when adding the check so future
regressions are detected explicitly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/planner/core/tests/pointget/trivial_plan_test.go`:
- Around line 56-104: Add an assertion in TestTrivialPlanFallback to directly
verify the trivial-plan fast path was not used by checking a test-only
observable (e.g., a planner metric counter or session variable). Instrument the
planner (e.g., increment a planner.TrivialPlanCounter or set a boolean flag when
trivial path is taken) and in the test read that observable before/after each
query in TestTrivialPlanFallback to assert it did not increase (or the flag is
false); reference TestTrivialPlanFallback and the planner observable (e.g.,
TrivialPlanCounter / trivialPathTaken) when adding the check so future
regressions are detected explicitly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 63ee5eed-bac7-4366-867a-ace7fd2aedf0

📥 Commits

Reviewing files that changed from the base of the PR and between cdad4f7 and dd47425.

📒 Files selected for processing (5)
  • pkg/planner/core/BUILD.bazel
  • pkg/planner/core/tests/pointget/BUILD.bazel
  • pkg/planner/core/tests/pointget/trivial_plan_test.go
  • pkg/planner/core/trivial_plan.go
  • pkg/planner/optimize.go

Tables with foreign keys have cascade/restrict behavior that needs the
full optimizer. Tables with hidden columns (used by expression indexes)
can cause schema mismatches in the fast path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/planner/core/trivial_plan.go`:
- Around line 253-261: In trivialRowCountEstimate, don't treat RealtimeCount==0
as pseudo; instead check the stats table's pseudo flag and return PseudoRowCount
only when statsTbl.IsPseudo is true; otherwise return
float64(statsTbl.RealtimeCount) (which will be 0 for genuinely empty tables).
Update the logic around domain.GetDomain(ctx).StatsHandle(),
statsHandle.GetPhysicalTableStats(tblInfo.ID, tblInfo), statsTbl.RealtimeCount
and statsTbl.IsPseudo so empty tables yield 0 and only truly pseudo stats yield
statistics.PseudoRowCount.
- Around line 170-183: PhysicalTableScan constructed in trivial_plan.go is
missing the PhysicalTableID assignment; when isTrivialTable ensures
non-partitioned tables you must set PhysicalTableID to the table ID. Update the
PhysicalTableScan initialization (the struct literal used to build ts in
trivial_plan.go) to include PhysicalTableID: tblInfo.ID so it matches other
construction sites (see planbuilder usage) and retains consistency with
ts.Init/SetSchema/SetStats flows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 428d5672-bb8c-4626-a464-492d568b8be7

📥 Commits

Reviewing files that changed from the base of the PR and between dd47425 and cfe8b02.

📒 Files selected for processing (1)
  • pkg/planner/core/trivial_plan.go

Comment thread pkg/planner/core/trivial_plan.go
Comment thread pkg/planner/core/trivial_plan.go Outdated
if col.State == model.StatePublic {
allColumns = append(allColumns, col)
tblCols = append(tblCols, colInfoToColumn(col, i))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Trivial plan breaks SELECT output column mapping

Why: PhysicalTableScan.Columns is derived from an ID-set in table order, dropping select-list ordering/duplicates/pseudo columns. This causes queries like SELECT c,a, SELECT a,a, SELECT _tidb_rowid, or SELECT *,a to return wrong column order or fail with schema/codec mismatches.

Evidence: pkg/planner/core/trivial_plan.go:129 builds Columns by deduping on col.ID and iterating in table order:

schemaColIDs[col.ID] = struct{}{}
...
for _, col := range allColumns {
    if _, ok := schemaColIDs[col.ID]; ok {
        columns = append(columns, col)
    }
}

This loses SELECT field ordering and duplicates. The scan output won't match the schema positions, causing wrong results.

// This avoids all logical optimization rules, stats loading, cost estimation,
// and post-optimization passes that are unnecessary when the plan is predetermined.
// It is designed for execution performance, not EXPLAIN accuracy.
func TryTrivialPlan(ctx base.PlanContext, node *resolve.NodeW) (base.Plan, types.NameSlice) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Trivial plan skips UnionScan requirements

Why: Returns a plain PhysicalTableReader even when dirty-txn reads or local temp/cache tables require UnionScan. For local temp tables, the TableReader runs in dummy mode and returns no rows. For dirty transactions, this breaks read-your-writes semantics.

Evidence: TryTrivialPlan returns before logical plan building that would add UnionScan at pkg/planner/core/logical_plan_builder.go:5276:

if dirty || tableInfo.TempTableType == model.TempTableLocal || 
   tableInfo.TableCacheStatusType == model.TableCacheStatusEnable {
    ... LogicalUnionScan ...
}

The early return at pkg/planner/optimize.go:264 bypasses this critical wrapping.

Comment thread pkg/planner/optimize.go
// Try the trivial plan fast path for simple full-table scans where the
// plan is predetermined (no secondary indexes, no TiFlash, no predicates).
// This skips logical optimization, stats loading, and cost estimation.
if tp, tpNames := core.TryTrivialPlan(pctx, node); tp != nil {
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] Trivial plan ignores sql_select_limit

Why: core.TryAddExtraLimit is applied only after the fast-path returns, so trivial plan can return unlimited rows when sql_select_limit is set (non-default configuration).

Evidence: pkg/planner/optimize.go:264 returns early:

if tp, tpNames := core.TryTrivialPlan(pctx, node); tp != nil {
    return tp, tpNames, nil
}

vs pkg/planner/optimize.go:276 which applies the limit:

stmtNode = core.TryAddExtraLimit(sctx, stmtNode)

The fast path skips this rewrite entirely.

if tnW == nil {
return nil, nil
}
tblInfo := tnW.TableInfo
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Trivial plan bypasses table-source semantics validation

Why: Can scan VIEWs, SEQUENCEs, or explicit PARTITION clauses instead of erroring/expanding/converting, changing results or suppressing expected errors.

Evidence: pkg/planner/core/trivial_plan.go:83 never checks tblInfo.IsView()/IsSequence()/IsBaseTable() before building a PhysicalTableScan. Normal planner at pkg/planner/core/logical_plan_builder.go:5006 expands views via BuildDataSourceFromView and at :5041 converts sequences to TableDual. The fast path bypasses all of this, producing empty/wrong results for SELECT * FROM view.

require.Len(t, rows, 3)

// Verify EXPLAIN shows a table scan plan.
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.

@terry1purcell terry1purcell changed the title planner: add trivial plan fast path for simple full-table scans (wip) planner: add trivial plan fast path for simple full-table scans Apr 5, 2026
@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented Apr 5, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign terry1purcell for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
pkg/planner/core/trivial_plan.go (1)

164-171: Consider adding a defensive nil check when building scan columns.

The allColumns slice (lines 153-158) filters columns by StatePublic, but buildSchemaFromFields doesn't apply this filter when expanding wildcards. During concurrent DDL (column addition in progress), a schema column could have an ID that doesn't exist in colInfoByID, causing a nil entry in the columns slice.

While MDL should prevent most concurrent DDL interference, a defensive check would prevent potential panics in edge cases.

Optional defensive fix
 columns := make([]*model.ColumnInfo, 0, schema.Len())
 for _, col := range schema.Columns {
-    columns = append(columns, colInfoByID[col.ID])
+    colInfo := colInfoByID[col.ID]
+    if colInfo == nil {
+        // Column not in public state; fall back to full optimizer.
+        return nil, nil
+    }
+    columns = append(columns, colInfo)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/trivial_plan.go` around lines 164 - 171, The code builds
columns from schema.Columns using colInfoByID but doesn't guard against missing
entries, which can produce nils and panic; in the block that iterates
schema.Columns (using colInfoByID, allColumns, and the columns slice), add a
defensive nil check for colInfoByID[col.ID] and either skip nil entries or
return a clear error (depending on the surrounding function's error handling)
instead of appending a nil pointer; ensure any chosen behavior preserves caller
expectations and consider logging or wrapping the error with context mentioning
the missing column ID.
pkg/sessionctx/variable/session.go (1)

1416-1419: Add SQL integration coverage for last_plan_from_trivial behavior.

This is a user-visible session-variable/protocol behavior change. Please add SQL integration tests for statement rollover semantics (current vs previous statement) and EXPLAIN bypass behavior.

I can draft a compact test matrix for these scenarios if you want.
Based on learnings: In pingcap/tidb, for session variables or protocol behavior changes, perform targeted package tests plus SQL integration tests for user-visible behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/sessionctx/variable/session.go` around lines 1416 - 1419, Tests are
missing for the user-visible session-variable/protocol behavior introduced by
FoundInTrivialPlan and PrevFoundInTrivialPlan (exposed as
last_plan_from_trivial); add targeted unit/package tests and SQL integration
tests that validate statement rollover semantics (current vs previous statement)
and EXPLAIN bypass behavior: write package-level tests exercising the session
state transitions in the code paths that set FoundInTrivialPlan and
PrevFoundInTrivialPlan, and add SQL integration tests that run sequences of
statements and EXPLAIN queries to assert last_plan_from_trivial toggles as
expected across consecutive statements and that EXPLAIN does not update the
trivial-plan flag. Ensure the tests reference the session variable name
last_plan_from_trivial and the internal flags FoundInTrivialPlan /
PrevFoundInTrivialPlan so failures point to the correct code paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/sessionctx/variable/session.go`:
- Around line 1416-1419: The new boolean fields FoundInTrivialPlan and
PrevFoundInTrivialPlan must be persisted during session encoding/decoding:
update EncodeSessionStates to write these two flags into the session state blob
(same area where plan-cache/bindings flags are serialized) and update
DecodeSessionStates to read them back and assign to the corresponding fields on
the session variable struct; reference the FoundInTrivialPlan and
PrevFoundInTrivialPlan fields when adding serialization/deserialization, and
ensure defaulting/validation matches the existing pattern used for the
neighboring plan-cache/binding flags.

In `@pkg/sessionctx/variable/sysvar.go`:
- Around line 421-423: The new session-visible sysvar TiDBFoundInTrivial
(GetSession uses s.PrevFoundInTrivialPlan) isn't persisted in session
snapshot/restore paths; update the session state serialization to include
PrevFoundInTrivialPlan alongside FoundInPlanCache and FoundInBinding.
Concretely: add a PrevFoundInTrivialPlan field to the session-states struct,
include it in the logic that ShowSessionStates/SetSessionStates (or the
functions that emit/consume session-states) use to serialize/deserialize session
state, and ensure the restore path assigns the value back to
SessionVars.PrevFoundInTrivialPlan so the sysvar remains consistent across
SHOW/SET SESSION_STATES.

---

Nitpick comments:
In `@pkg/planner/core/trivial_plan.go`:
- Around line 164-171: The code builds columns from schema.Columns using
colInfoByID but doesn't guard against missing entries, which can produce nils
and panic; in the block that iterates schema.Columns (using colInfoByID,
allColumns, and the columns slice), add a defensive nil check for
colInfoByID[col.ID] and either skip nil entries or return a clear error
(depending on the surrounding function's error handling) instead of appending a
nil pointer; ensure any chosen behavior preserves caller expectations and
consider logging or wrapping the error with context mentioning the missing
column ID.

In `@pkg/sessionctx/variable/session.go`:
- Around line 1416-1419: Tests are missing for the user-visible
session-variable/protocol behavior introduced by FoundInTrivialPlan and
PrevFoundInTrivialPlan (exposed as last_plan_from_trivial); add targeted
unit/package tests and SQL integration tests that validate statement rollover
semantics (current vs previous statement) and EXPLAIN bypass behavior: write
package-level tests exercising the session state transitions in the code paths
that set FoundInTrivialPlan and PrevFoundInTrivialPlan, and add SQL integration
tests that run sequences of statements and EXPLAIN queries to assert
last_plan_from_trivial toggles as expected across consecutive statements and
that EXPLAIN does not update the trivial-plan flag. Ensure the tests reference
the session variable name last_plan_from_trivial and the internal flags
FoundInTrivialPlan / PrevFoundInTrivialPlan so failures point to the correct
code paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5bee4f70-dc7c-4f38-a439-0fad5f6c1a5f

📥 Commits

Reviewing files that changed from the base of the PR and between cfe8b02 and e4648f4.

📒 Files selected for processing (7)
  • pkg/executor/select.go
  • pkg/planner/core/tests/pointget/trivial_plan_test.go
  • pkg/planner/core/trivial_plan.go
  • pkg/planner/optimize.go
  • pkg/sessionctx/vardef/tidb_vars.go
  • pkg/sessionctx/variable/session.go
  • pkg/sessionctx/variable/sysvar.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/planner/core/tests/pointget/trivial_plan_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/planner/optimize.go

Comment on lines +1416 to +1419
// FoundInTrivialPlan indicates whether this statement used the trivial plan fast path.
FoundInTrivialPlan bool
// PrevFoundInTrivialPlan indicates whether the last statement used the trivial plan fast path.
PrevFoundInTrivialPlan bool
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Persist the new trivial-plan flag through session state encode/decode.

FoundInTrivialPlan / PrevFoundInTrivialPlan are introduced here, but unlike the neighboring plan-cache/binding flags, they are not wired into EncodeSessionStates / DecodeSessionStates in this file. That can lose last_plan_from_trivial state across session migration.

💡 Proposed patch
@@ func (s *SessionVars) EncodeSessionStates(_ context.Context, sessionStates *sessionstates.SessionStates) (err error) {
 	sessionStates.FoundInPlanCache = s.PrevFoundInPlanCache
+	sessionStates.FoundInTrivialPlan = s.PrevFoundInTrivialPlan
 	sessionStates.FoundInBinding = s.PrevFoundInBinding
@@ func (s *SessionVars) DecodeSessionStates(_ context.Context, sessionStates *sessionstates.SessionStates) (err error) {
 	s.FoundInPlanCache = sessionStates.FoundInPlanCache
+	s.FoundInTrivialPlan = sessionStates.FoundInTrivialPlan
 	s.FoundInBinding = sessionStates.FoundInBinding
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/sessionctx/variable/session.go` around lines 1416 - 1419, The new boolean
fields FoundInTrivialPlan and PrevFoundInTrivialPlan must be persisted during
session encoding/decoding: update EncodeSessionStates to write these two flags
into the session state blob (same area where plan-cache/bindings flags are
serialized) and update DecodeSessionStates to read them back and assign to the
corresponding fields on the session variable struct; reference the
FoundInTrivialPlan and PrevFoundInTrivialPlan fields when adding
serialization/deserialization, and ensure defaulting/validation matches the
existing pattern used for the neighboring plan-cache/binding flags.

Comment on lines +421 to +423
{Scope: vardef.ScopeSession, Name: vardef.TiDBFoundInTrivialPlan, Value: BoolToOnOff(false), Type: vardef.TypeBool, ReadOnly: true, GetSession: func(s *SessionVars) (string, error) {
return BoolToOnOff(s.PrevFoundInTrivialPlan), nil
}},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Serialize trivial-plan state in session snapshot/restore paths.

This new previous-statement sysvar is session-visible, but the session-state flow currently serializes/restores FoundInPlanCache / FoundInBinding only. @@last_plan_from_trivial can be dropped or become inconsistent after SHOW SESSION_STATES + SET SESSION_STATES.

Suggested follow-up (outside this hunk)
--- a/pkg/sessionctx/variable/session.go
+++ b/pkg/sessionctx/variable/session.go
@@
 	sessionStates.FoundInPlanCache = s.PrevFoundInPlanCache
+	sessionStates.FoundInTrivialPlan = s.PrevFoundInTrivialPlan
 	sessionStates.FoundInBinding = s.PrevFoundInBinding
@@
 	s.FoundInPlanCache = sessionStates.FoundInPlanCache
+	s.FoundInTrivialPlan = sessionStates.FoundInTrivialPlan
 	s.FoundInBinding = sessionStates.FoundInBinding

Also add the corresponding field in the session-states struct if it does not exist yet.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/sessionctx/variable/sysvar.go` around lines 421 - 423, The new
session-visible sysvar TiDBFoundInTrivial (GetSession uses
s.PrevFoundInTrivialPlan) isn't persisted in session snapshot/restore paths;
update the session state serialization to include PrevFoundInTrivialPlan
alongside FoundInPlanCache and FoundInBinding. Concretely: add a
PrevFoundInTrivialPlan field to the session-states struct, include it in the
logic that ShowSessionStates/SetSessionStates (or the functions that
emit/consume session-states) use to serialize/deserialize session state, and
ensure the restore path assigns the value back to
SessionVars.PrevFoundInTrivialPlan so the sysvar remains consistent across
SHOW/SET SESSION_STATES.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
pkg/planner/optimize.go (1)

266-280: ⚠️ Potential issue | 🟠 Major

Still bypasses sql_select_limit on the trivial return path.

core.TryAddExtraLimit is only applied at Lines 291-293, so returning here can still ignore a non-default sql_select_limit and scan/return the full table. Please run this fast path after the extra-limit rewrite, or suppress it when an implicit limit would be injected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/optimize.go` around lines 266 - 280, The trivial-plan fast path
in the TryTrivialPlan branch returns before applying the extra-limit rewrite, so
a non-default sql_select_limit can be bypassed; modify the code in the
TryTrivialPlan handling (where TryTrivialPlan, CheckTableMode and
sessVars.FoundInTrivialPlan are used) to invoke the same extra-limit rewrite
logic (i.e., call core.TryAddExtraLimit or equivalent used later) before
returning the trivial plan, or alternatively detect when an implicit limit would
be injected and suppress the trivial-path return in that case so the extra-limit
rewrite runs; ensure the behavior matches the later branch that currently
applies TryAddExtraLimit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pkg/planner/optimize.go`:
- Around line 266-280: The trivial-plan fast path in the TryTrivialPlan branch
returns before applying the extra-limit rewrite, so a non-default
sql_select_limit can be bypassed; modify the code in the TryTrivialPlan handling
(where TryTrivialPlan, CheckTableMode and sessVars.FoundInTrivialPlan are used)
to invoke the same extra-limit rewrite logic (i.e., call core.TryAddExtraLimit
or equivalent used later) before returning the trivial plan, or alternatively
detect when an implicit limit would be injected and suppress the trivial-path
return in that case so the extra-limit rewrite runs; ensure the behavior matches
the later branch that currently applies TryAddExtraLimit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 09fa004c-4fa8-4b15-a0a3-d3a555332c9c

📥 Commits

Reviewing files that changed from the base of the PR and between e4648f4 and 7c02742.

📒 Files selected for processing (1)
  • pkg/planner/optimize.go

Previously the trivial plan fast path bailed on any table with a secondary
index and on any query with a WHERE clause. That left a lot of provably-
table-scan queries paying the full optimizer cost.

This change replaces the table-level "no indexes" rule with a per-query
gate: the fast path now applies whenever no index could plausibly be
chosen — i.e. no index's leading column is referenced by a predicate and
no index covers the SELECT projection. References to the primary key
(handle or clustered) are still treated as range-scan candidates and
disqualify the fast path.

WHERE clauses are supported by walking the predicate AST to collect
referenced columns, extending the scan column set, rewriting the
predicate against that schema, and wrapping a PhysicalSelection above
the PhysicalTableScan. Subqueries, aggregates, window functions, and
session variables in WHERE keep the fast path out. A PhysicalProjection
is added only when WHERE-only columns need to be stripped from the
output.

Because every condition is handed to the coprocessor via the Selection's
PB serialization (the trivial path doesn't split conditions between
TiKV-side and TiDB-side evaluation), bail when any condition isn't
TiKV-pushable so we don't surface a runtime ErrInternal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ti-chi-bot ti-chi-bot Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels May 26, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/planner/core/trivial_plan.go (1)

459-497: 💤 Low value

Consider disallowing ParamMarkerExpr in the WHERE clause visitor.

Prepared statement parameters (? placeholders) are not explicitly checked in whereColumnVisitor. While they would likely still produce correct results (pushdown check should pass), the trivial path may have subtle interactions with prepared statement execution that haven't been tested.

If prepared statements are intended to use the trivial path, add test coverage. Otherwise, adding *ast.ParamMarkerExpr to the disallowed cases would be a safe conservative choice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/planner/core/trivial_plan.go` around lines 459 - 497, The WHERE-clause
AST visitor whereColumnVisitor currently doesn't treat prepared-statement
parameter nodes as disallowed, which may let queries with ParamMarkerExprs take
the trivial fast path; update the Enter method of whereColumnVisitor to include
*ast.ParamMarkerExpr in the disallowed case list (alongside SubqueryExpr,
ExistsSubqueryExpr, CompareSubqueryExpr, AggregateFuncExpr, WindowFuncExpr,
VariableExpr) so v.disallowed is set and traversal stops for parameterized
WHEREs, or alternatively add tests proving safe acceptance if you intend to
allow them.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/planner/core/trivial_plan.go`:
- Around line 459-497: The WHERE-clause AST visitor whereColumnVisitor currently
doesn't treat prepared-statement parameter nodes as disallowed, which may let
queries with ParamMarkerExprs take the trivial fast path; update the Enter
method of whereColumnVisitor to include *ast.ParamMarkerExpr in the disallowed
case list (alongside SubqueryExpr, ExistsSubqueryExpr, CompareSubqueryExpr,
AggregateFuncExpr, WindowFuncExpr, VariableExpr) so v.disallowed is set and
traversal stops for parameterized WHEREs, or alternatively add tests proving
safe acceptance if you intend to allow them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2c9fd73a-f871-45b8-afcc-8cc81dcd7171

📥 Commits

Reviewing files that changed from the base of the PR and between 7c02742 and 069ba47.

📒 Files selected for processing (3)
  • pkg/planner/core/tests/pointget/BUILD.bazel
  • pkg/planner/core/tests/pointget/trivial_plan_test.go
  • pkg/planner/core/trivial_plan.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/planner/core/tests/pointget/BUILD.bazel

terry1purcell and others added 4 commits May 26, 2026 15:06
The full optimizer's CollectPredicateColumnsPoint rule registers WHERE-
referenced columns for sync or async stats loading. The trivial path
skipped this rule entirely, so queries that should have triggered a
sync wait (or queued an async load) silently bypassed it.

Use the same Table.ColumnIsLoadNeeded check the rule uses: if any
WHERE column has analyzed stats that are evicted or partially loaded,
fall back to the full planner so the load registration happens.

Fixes TestLoadAnalyzeV1StatsJSONFromV855 in tests/realtikvtest/statisticstest,
which exercises both sync (tidb_stats_load_sync_wait=60000) and async
(tidb_stats_load_sync_wait=0) paths on a query whose WHERE column has
analyzed-but-evicted stats.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The full optimizer's CollectPredicateColumnsPoint rule also calls
UpdateColStatsUsage so ANALYZE PREDICATE COLUMNS knows which columns
were referenced by predicates. The trivial path skipped this side
effect, so a SELECT … WHERE col = X that took the fast path stopped
contributing to the session's predicate-column collector.

The previous commit's load-needed gate only addresses the subset where
column stats are evicted; predicate tracking happens for all WHERE
references regardless of load state.

Fixes TestCleanupPredicateColumns, TestAnalyzeTableWithPredicateColumns,
TestAnalyzeTableWithTiDBPersistAnalyzeOptionsDisabled,
TestPredicateUsage_FirstTouchCreatesRow,
TestPredicateUsage_NoBumpWithinThrottle in pkg/statistics/handle/usage
and TestInitStatsVer2 in pkg/statistics/handle/handletest/statstest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
genBriefPlanWithSCtx is called by the binding system (EXPLAIN EXPLORE,
auto-binding suggestions) to produce plan variants whose hints can be
extracted via GenHintsFromFlatPlan. Optimizer fast paths — TryFastPlan
for point gets and TryTrivialPlan for full table scans — produce bare
operator trees with no hint metadata, which breaks downstream hint
parsing ("Optimizer hint syntax error" from an empty /*+ */ comment).

Set StmtCtx.InExplainStmt = true before invoking Optimize so the
existing InExplainStmt gate in TryTrivialPlan (and any future fast
path) skips itself and lets the full planner produce the richer
metadata the binding consumer expects.

Fixes TestExplainExploreAnalyze and TestExplainExploreVerifyAndBind
in pkg/bindinfo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PlanBuilder.buildSelection (logical_plan_builder.go:1386) detects when
a CNF predicate's eval type is string-typed (enum, set, json,
hybrid-typed control functions) and wraps it via
TryPushCastIntoControlFunctionForHybridType so the runtime evaluates
truthiness against the underlying numeric representation.

The trivial path did not apply that transformation, which made
WHERE clauses like `if(e>1, e, e)` on an enum column always evaluate
to false — the raw enum value would be read as 0 instead of its
index. Result: trivial path returned zero rows where the full
planner correctly returned all matching rows.

Mirror the same cast on the CNF items before pushdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented May 27, 2026

@terry1purcell: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
idc-jenkins-ci-tidb/mysql-test dc8a3ca link true /test mysql-test
idc-jenkins-ci-tidb/unit-test dc8a3ca link true /test unit-test
pull-unit-test-next-gen dc8a3ca link true /test pull-unit-test-next-gen
idc-jenkins-ci-tidb/check_dev dc8a3ca link true /test check-dev
idc-jenkins-ci-tidb/check_dev_2 dc8a3ca link true /test check-dev2
pull-integration-realcluster-test-next-gen dc8a3ca link true /test pull-integration-realcluster-test-next-gen

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note-none Denotes a PR that doesn't merit a release note. sig/planner SIG: Planner size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant