Skip to content

pkg/planner, pkg/sessionctx: support non-decorrelate alternative#66995

Merged
ti-chi-bot[bot] merged 2 commits into
pingcap:masterfrom
AilinKid:alternative-logical-plans
Mar 25, 2026
Merged

pkg/planner, pkg/sessionctx: support non-decorrelate alternative#66995
ti-chi-bot[bot] merged 2 commits into
pingcap:masterfrom
AilinKid:alternative-logical-plans

Conversation

@AilinKid
Copy link
Copy Markdown
Contributor

@AilinKid AilinKid commented Mar 13, 2026

What problem does this PR solve?

Issue Number: ref #65710

Problem Summary:

The optimizer only explored one logical-plan shape, so correlated subqueries could not keep a non-decorrelated alternative and compare physical costs. The regression coverage for this path also depended on importing a full plan replayer.

What changed and how does it work?

  • add session variable tidb_opt_enable_alternative_logical_plans to gate alternative logical-plan exploration
  • in planner.Optimize, when enabled, run an extra logical optimization round with decorrelate disabled, physically optimize both candidates, and keep the lower-cost plan
  • add planner regressions that verify the lower-cost Apply path can be selected
  • replace the replayer-style regression with an anonymized correlated casetest backed by minimal schema + trimmed stats json

Example plans

A Customer Query Example:

SELECT c.id
FROM fixture_c c
WHERE EXISTS (
    SELECT 1
    FROM fixture_a a, fixture_b b
    WHERE a.id = b.order_id
      AND a.id = c.order_id
      AND b.raise_end_date <= '20250512'
      AND a.sub_trans_code IN ('0103')
      AND a.create_time >= '2025-04-12 00:00:00.0'
      AND a.extend_scen = '37'
)
  AND c.phase_zone = 'Recharge2'
  AND (c.data_type = 'N' OR c.data_type IS NULL)
  AND c.latest_status IS NULL
  AND c.create_time >= '2025-04-12 00:00:00.0'
ORDER BY c.create_time ASC
LIMIT 50;

tidb_opt_enable_alternative_logical_plans = OFF

TopN 38.39 root  fixture_db.fixture_c.create_time, offset:0, count:50
└─HashJoin 38.39 root  semi join, left side:IndexLookUp, equal:[eq(fixture_db.fixture_c.order_id, fixture_db.fixture_a.id)]
  ├─IndexJoin(Build) 1.00 root  inner join, inner:IndexLookUp, outer key:fixture_db.fixture_b.order_id, inner key:fixture_db.fixture_a.id, equal cond:eq(fixture_db.fixture_b.order_id, fixture_db.fixture_a.id)
  │ ├─TableReader(Build) 173980.58 root  data:Selection
  │ │ └─Selection 173980.58 cop[tikv]  le(fixture_db.fixture_b.raise_end_date, "20250512")
  │ │   └─TableFullScan 372338448.00 cop[tikv] table:b keep order:false
  │ └─IndexLookUp(Probe) 1.00 root
  │   ├─IndexRangeScan(Build) 173980.58 cop[tikv] table:a, index:PRIMARY(id) range: decided by [eq(fixture_db.fixture_a.id, fixture_db.fixture_b.order_id)], keep order:false
  │   └─Selection(Probe) 1.00 cop[tikv]  eq(fixture_db.fixture_a.extend_scen, "37"), eq(fixture_db.fixture_a.sub_trans_code, "0103"), ge(fixture_db.fixture_a.create_time, 2025-04-12 00:00:00.000000)
  │     └─TableRowIDScan 173980.58 cop[tikv] table:a keep order:false
  └─IndexLookUp(Probe) 47.99 root
    ├─IndexRangeScan(Build) 2442.31 cop[tikv] table:c, index:idx_fixture_c_ls_ct(latest_status, create_time) range:[NULL 2025-04-12 00:00:00,NULL +inf], keep order:false
    └─Selection(Probe) 47.99 cop[tikv]  eq(fixture_db.fixture_c.phase_zone, "Recharge2"), or(eq(fixture_db.fixture_c.data_type, "N"), isnull(fixture_db.fixture_c.data_type))
      └─TableRowIDScan 2442.31 cop[tikv] table:c keep order:false

tidb_opt_enable_alternative_logical_plans = ON

Limit 47.99 root  offset:0, count:50
└─Apply 47.99 root  CARTESIAN semi join, left side:IndexLookUp
  ├─IndexLookUp(Build) 47.99 root
  │ ├─IndexRangeScan(Build) 2442.31 cop[tikv] table:c, index:idx_fixture_c_ls_ct(latest_status, create_time) range:[NULL 2025-04-12 00:00:00,NULL +inf], keep order:true
  │ └─Selection(Probe) 47.99 cop[tikv]  eq(fixture_db.fixture_c.phase_zone, "Recharge2"), or(eq(fixture_db.fixture_c.data_type, "N"), isnull(fixture_db.fixture_c.data_type))
  │   └─TableRowIDScan 2442.31 cop[tikv] table:c keep order:false
  └─MergeJoin(Probe) 47.99 root  inner join, left key:fixture_db.fixture_a.id, right key:fixture_db.fixture_b.order_id
    ├─Projection(Build) 47.99 root  fixture_db.fixture_b.order_id, fixture_db.fixture_b.raise_end_date
    │ └─IndexLookUp 47.99 root
    │   ├─IndexRangeScan(Build) 47.99 cop[tikv] table:b, index:idx_uk_fixture_b_oid(order_id) range: decided by [eq(fixture_db.fixture_b.order_id, fixture_db.fixture_c.order_id)], keep order:true
    │   └─Selection(Probe) 47.99 cop[tikv]  le(fixture_db.fixture_b.raise_end_date, "20250512")
    │     └─TableRowIDScan 47.99 cop[tikv] table:b keep order:false
    └─Projection(Probe) 47.99 root  fixture_db.fixture_a.id, fixture_db.fixture_a.sub_trans_code, fixture_db.fixture_a.extend_scen, fixture_db.fixture_a.create_time
      └─IndexLookUp 47.99 root
        ├─IndexRangeScan(Build) 47.99 cop[tikv] table:a, index:PRIMARY(id) range: decided by [eq(fixture_db.fixture_a.id, fixture_db.fixture_c.order_id)], keep order:true
        └─Selection(Probe) 47.99 cop[tikv]  eq(fixture_db.fixture_a.extend_scen, "37"), eq(fixture_db.fixture_a.sub_trans_code, "0103"), ge(fixture_db.fixture_a.create_time, 2025-04-12 00:00:00.000000)
          └─TableRowIDScan 47.99 cop[tikv] table:a keep order:false

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

None

Summary by CodeRabbit

  • New Features

    • Added a new system variable to toggle alternative logical plan selection.
  • Tests

    • Added extensive unit and integration tests validating correlated-subquery plan choices, alternative-plan rounds, and correctness with loaded statistics.
    • Added tests ensuring EXISTS/NOT EXISTS shapes skip unnecessary alternative-plan rounds.
  • Chores

    • Added deterministic test fixtures and helper utilities to drive reproducible plan/result comparisons.

@ti-chi-bot ti-chi-bot Bot added the release-note-none Denotes a PR that doesn't merit a release note. label Mar 13, 2026
@pantheon-ai
Copy link
Copy Markdown

pantheon-ai Bot commented Mar 13, 2026

Review failed due to infrastructure/execution failure after retries. Please re-trigger review.

ℹ️ Learn more details on Pantheon AI.

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

tiprow Bot commented Mar 13, 2026

Hi @AilinKid. 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 13, 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 an optional second logical-plan optimization round driven by a decorrelate-change signal, snapshots/restores planner state across rounds, threads decorrelate/outer-driven-index-join signals through optimization, marks decorrelated semi-apply joins for index-join generation, and adds tests/fixtures plus a session toggle to enable alternative logical-plan rounds.

Changes

Cohort / File(s) Summary
Correlated subquery tests & fixtures
pkg/planner/core/casetest/correlated/correlated_test.go, pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_*.json, pkg/planner/core/casetest/correlated/testdata/fixture_*.sql
Added two correlated-subquery tests that prepare fixtures and load table stats, plus three SQL fixture files and expected plan/result JSON entries.
Integration & planner tests
pkg/planner/core/integration_test.go, pkg/planner/core/physical_plan_test.go, pkg/sessionctx/stmtctx/stmtctx_test.go, pkg/sessionctx/variable/varsutil_test.go
New integration/unit tests checking alternative-round skipping for decorrelated index-joins, warning/state clearing across optimize rounds, and helper utilities for explain output scanning.
Optimizer multi-round control flow
pkg/planner/optimize.go, pkg/planner/core/optimizer.go
Introduce per-round logical-plan build/restore baselines, pass decorrelateChanged/foundEquivalentOuterDrivenIndexJoin signals, run optional second round without decorrelate, and select lower-cost plan across rounds.
Decorrelation rule changes
pkg/planner/core/rule_decorrelate.go
Ensure decorrelation steps mark planChanged, add optimizeWithCurrentPlanChanged, introduce decorrelateRuleName and helper to detect simple semi-apply patterns used to mark joins.
Logical & physical join flags
pkg/planner/core/operator/logicalop/logical_join.go, pkg/planner/core/operator/physicalop/physical_index_join.go, pkg/planner/core/exhaust_physical_plans.go
Add LogicalJoin.FromSimpleSemiApply and PhysicalIndexJoin.FromDecorrelatedSimpleSemiApply fields; propagate through clone and initialize when constructing index join from a decorrelated simple semi-apply.
StmtCtx snapshot/restore & state helpers
pkg/sessionctx/stmtctx/stmtctx.go, pkg/sessionctx/stmtctx/stmtctx_test.go
Add LogicalPlanBuildBaseline and APIs to save/restore baseline, prepare fresh logical-build state, capture logical-build state, and tests validating the reset/restore semantics.
Session/sysvar wiring
pkg/sessionctx/vardef/tidb_vars.go, pkg/sessionctx/variable/session.go, pkg/sessionctx/variable/sysvar.go, pkg/sessionctx/variable/setvar_affect.go
Add tidb_opt_enable_alternative_logical_plans system/session variable (default false) and wire into SessionVars.EnableAlternativeLogicalPlans and hint-update verification.
Build/test config
pkg/planner/BUILD.bazel, pkg/planner/core/casetest/correlated/BUILD.bazel, pkg/sessionctx/stmtctx/BUILD.bazel
Updated planner bazel deps, increased test shard counts, and added test deps for domain and testify require.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Optimizer as Optimizer (optimize)
    participant LogicalOpt as Logical Optimizer
    participant Decorrelate as Decorrelate Solver
    participant PhysicalOpt as Physical Optimizer

    Client->>Optimizer: optimize(logicalPlan)
    Optimizer->>Optimizer: PrepareLogicalPlanBuildState()\nSaveLogicalPlanBuildBaseline()
    Optimizer->>LogicalOpt: buildAndOptimizeLogicalPlanRound(round=0)
    LogicalOpt->>Decorrelate: apply decorrelation rules
    Decorrelate-->>LogicalOpt: decorrelateChanged (true/false)
    LogicalOpt-->>Optimizer: (logicalPlan, decorrelateChanged, foundEquivalentOuterDrivenIndexJoin)
    Optimizer->>PhysicalOpt: physical optimization (constructIndexJoin uses FromDecorrelatedSimpleSemiApply)
    PhysicalOpt-->>Optimizer: physicalPlan, cost

    alt decorrelateChanged && !foundEquivalentOuterDrivenIndexJoin && EnableAlternativeLogicalPlans
        Optimizer->>Optimizer: RestoreLogicalPlanBuildBaseline()
        Optimizer->>LogicalOpt: buildAndOptimizeLogicalPlanRound(round=withoutDecorrelate)
        LogicalOpt-->>Optimizer: alternativeLogicalPlan
        Optimizer->>PhysicalOpt: physical optimization (alternative)
        PhysicalOpt-->>Optimizer: alternativePhysicalPlan, cost2
        Optimizer->>Optimizer: choose lower-cost physical plan
    end

    Optimizer-->>Client: finalPhysicalPlan, cost
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested Reviewers

  • guo-shaoge
  • qw4990
  • hawkingrei
  • terry1purcell

Poem

🐰 I hopped through plans both old and new,
I cached a baseline, then built one or two,
If decorrelate flips, I peek and restore,
Choose the cheaper path and nuzzle the score.
Tests munch carrots — optimizer, encore!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding support for non-decorrelated alternative logical plans to the planner and session context.
Description check ✅ Passed The description covers all required template sections: problem statement (Issue #65710), detailed changes, example plans, test coverage checklist, side effects, and documentation notes. All mandatory fields are present and substantive.

✏️ 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.

@hawkingrei
Copy link
Copy Markdown
Member

/ok-to-test

@ti-chi-bot ti-chi-bot Bot added the ok-to-test Indicates a PR is ready to be tested. label Mar 13, 2026
@AilinKid AilinKid changed the title pkg/planner, pkg/sessionctx: support alternative logical plans pkg/planner, pkg/sessionctx: support non-decorrelate alternative Mar 13, 2026
@AilinKid
Copy link
Copy Markdown
Contributor Author

/test all

Comment thread pkg/planner/optimize.go Outdated
// allow change variables (otherwise can't unset read-only mode)
case *ast.SetStmt,
// allow analyze table
// allow analyze table
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you should run gofmt.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/planner/optimize.go (1)

495-550: ⚠️ Potential issue | 🟡 Minor

Only the winner round should emit unused-view warnings.

Lines 495-496 still defer HandleUnusedViewHints() for every round. With tidb_opt_enable_alternative_logical_plans=on, the same AST is built twice, so unused view-hint warnings will be appended twice even though only one round can win. The winner snapshot at Lines 549-550 is also taken before that defer fires, so replaying warnings from the chosen round would be safer.

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

In `@pkg/planner/optimize.go` around lines 495 - 550, The defer
builder.HandleUnusedViewHints() is running for every optimization round and
causing duplicate unused-view warnings; remove that unconditional defer and
instead invoke builder.HandleUnusedViewHints() only when the round becomes the
winner (inside the if block that updates *bestPlan / *bestCost / *bestNames),
and also call it before returning a non-logical plan (where p is not a
base.LogicalPlan) so warnings are emitted only for the chosen plan; use the
existing symbols builder.HandleUnusedViewHints(), buildLogicalPlan, and the
bestPlan/bestCost update path (and take care to call it after you save/clone any
logical-plan context like bestLogicalPlanCtx so the winner snapshot is
preserved).
🧹 Nitpick comments (1)
pkg/sessionctx/vardef/tidb_vars.go (1)

337-340: Document the optimization CPU trade-off in this variable comment.

Line 337 explains behavior, but not the known extra optimization CPU overhead when enabled.

✏️ Suggested comment update
-	// TiDBOptEnableAlternativeLogicalPlans controls whether the optimizer evaluates
-	// extra logical-plan alternatives and chooses the lower-cost plan.
+	// TiDBOptEnableAlternativeLogicalPlans controls whether the optimizer evaluates
+	// extra logical-plan alternatives and chooses the lower-cost plan.
+	// Enabling it may increase optimization CPU due to extra planning rounds.
 	TiDBOptEnableAlternativeLogicalPlans = "tidb_opt_enable_alternative_logical_plans"
As per coding guidelines "Comments SHOULD explain non-obvious intent, constraints, invariants, concurrency guarantees, SQL/compatibility contracts, or important performance trade-offs".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/sessionctx/vardef/tidb_vars.go` around lines 337 - 340, Update the
comment for the TiDBOptEnableAlternativeLogicalPlans variable to document the
CPU/performance trade-off: explicitly state that enabling extra logical-plan
alternatives can increase optimizer CPU usage and planning latency (especially
on complex queries or high-concurrency workloads), and note any situations where
the extra planning cost is expected to outweigh runtime gains; edit the comment
block above the TiDBOptEnableAlternativeLogicalPlans constant to include this
guidance and any relevant caveats for operators.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@pkg/planner/optimize.go`:
- Around line 495-550: The defer builder.HandleUnusedViewHints() is running for
every optimization round and causing duplicate unused-view warnings; remove that
unconditional defer and instead invoke builder.HandleUnusedViewHints() only when
the round becomes the winner (inside the if block that updates *bestPlan /
*bestCost / *bestNames), and also call it before returning a non-logical plan
(where p is not a base.LogicalPlan) so warnings are emitted only for the chosen
plan; use the existing symbols builder.HandleUnusedViewHints(),
buildLogicalPlan, and the bestPlan/bestCost update path (and take care to call
it after you save/clone any logical-plan context like bestLogicalPlanCtx so the
winner snapshot is preserved).

---

Nitpick comments:
In `@pkg/sessionctx/vardef/tidb_vars.go`:
- Around line 337-340: Update the comment for the
TiDBOptEnableAlternativeLogicalPlans variable to document the CPU/performance
trade-off: explicitly state that enabling extra logical-plan alternatives can
increase optimizer CPU usage and planning latency (especially on complex queries
or high-concurrency workloads), and note any situations where the extra planning
cost is expected to outweigh runtime gains; edit the comment block above the
TiDBOptEnableAlternativeLogicalPlans constant to include this guidance and any
relevant caveats for operators.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 023ba60e-6618-4b2c-a7bd-5902206cd0c2

📥 Commits

Reviewing files that changed from the base of the PR and between cc8c83b and e6ae2ff.

📒 Files selected for processing (17)
  • pkg/planner/core/casetest/correlated/correlated_test.go
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_in.json
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_out.json
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_xut.json
  • pkg/planner/core/casetest/correlated/testdata/fixture_a.sql
  • pkg/planner/core/casetest/correlated/testdata/fixture_b.sql
  • pkg/planner/core/casetest/correlated/testdata/fixture_c.sql
  • pkg/planner/core/casetest/correlated/testdata/fixture_db.fixture_a.json
  • pkg/planner/core/casetest/correlated/testdata/fixture_db.fixture_b.json
  • pkg/planner/core/casetest/correlated/testdata/fixture_db.fixture_c.json
  • pkg/planner/core/integration_test.go
  • pkg/planner/optimize.go
  • pkg/sessionctx/vardef/tidb_vars.go
  • pkg/sessionctx/variable/session.go
  • pkg/sessionctx/variable/setvar_affect.go
  • pkg/sessionctx/variable/sysvar.go
  • pkg/sessionctx/variable/varsutil_test.go

Comment thread pkg/planner/core/casetest/correlated/correlated_test.go
Comment thread pkg/planner/core/integration_test.go Outdated

tk.MustExec("set @@tidb_opt_enable_alternative_logical_plans=on")
alternativePlan := tk.MustQuery("explain format='brief' select t1.a, (select count(*) from t2 where t2.a = t1.a) as cnt from t1 order by t1.a").Rows()
require.Equal(t, hintedPlan, alternativePlan, caller+": enabled alternative round should pick the lower-cost undecorrelated plan")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we move this test case into casetest/correlated

Comment thread pkg/planner/optimize.go Outdated
return optFlag &^ rule.FlagDecorrelate
}
return optFlag
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we need figure out a better way so it's easier to extend to more round, instead of using hard-coded int value. We can do that in the future.

@ti-chi-bot ti-chi-bot Bot added the needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label Mar 13, 2026
@AilinKid
Copy link
Copy Markdown
Contributor Author

/retest-required

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.6006%. Comparing base (8e791c9) to head (ae951e2).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #66995        +/-   ##
================================================
- Coverage   77.7962%   77.6006%   -0.1957%     
================================================
  Files          2022       1942        -80     
  Lines        554862     542444     -12418     
================================================
- Hits         431662     420940     -10722     
- Misses       121455     121502        +47     
+ Partials       1745          2      -1743     
Flag Coverage Δ
integration 40.8895% <57.8125%> (-7.2270%) ⬇️
unit 76.7068% <100.0000%> (+0.3769%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
dumpling 61.5065% <ø> (ø)
parser ∅ <ø> (∅)
br 48.9047% <ø> (-11.9580%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ti-chi-bot ti-chi-bot Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. approved and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Mar 16, 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.

Actionable comments posted: 2

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

477-492: Signature expanded with multiple boolean signals.

The function now returns (base.Plan, types.NameSlice, nonLogical bool, decorrelateChanged bool, foundEquivalentOuterDrivenIndexJoin bool, error). Consider grouping these related signals into a small result struct for clarity and future extensibility, rather than having 6 return values.

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

In `@pkg/planner/optimize.go` around lines 477 - 492, The function
buildAndOptimizeLogicalPlanRound currently returns six separate values
(base.Plan, types.NameSlice, two/three booleans, and error); refactor by
introducing a small result struct (e.g., LogicalPlanRoundResult) that groups
these outputs as named fields (Plan base.Plan, Names types.NameSlice, NonLogical
bool, DecorrelateChanged bool, FoundEquivalentOuterDrivenIndexJoin bool, Err
error), update the buildAndOptimizeLogicalPlanRound signature to return that
struct (and remove the multi-value return), and update all call sites to use the
struct fields instead of positional returns; ensure you also update any
documentation/comments and preserve existing semantics and error handling when
populating and returning the new struct from buildAndOptimizeLogicalPlanRound.
pkg/planner/core/optimizer.go (1)

1005-1013: Magic string comparison for rule name is fragile.

The condition rule.Name() == "decorrelate" relies on a string literal. If the DecorrelateSolver.Name() method's return value changes, this logic would silently break. Consider using a constant or comparing against the rule type directly.

♻️ Suggested approach

Define a constant in the rule package and use it consistently:

// In pkg/planner/core/rule/constants.go or similar
const DecorrelateRuleName = "decorrelate"

Then use:

-if planChanged && rule.Name() == "decorrelate" {
+if planChanged && rule.Name() == rule.DecorrelateRuleName {

Alternatively, use a type assertion if feasible.

Also applies to: 1041-1043

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

In `@pkg/planner/core/optimizer.go` around lines 1005 - 1013, Replace the fragile
string literal check rule.Name() == "decorrelate" in the Optimize loop with a
stable identifier: either compare against a package-level constant (e.g.,
rule.DecorrelateRuleName) defined next to DecorrelateSolver.Name(), or perform a
type assertion/check against the concrete rule type (e.g., _, ok :=
rule.(*DecorrelateSolver)) to detect the decorrelate rule; update both
occurrences (the one after rule.Optimize and the later similar check) to use the
chosen approach so the logic no longer depends on a magic string.
🤖 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/casetest/correlated/correlated_test.go`:
- Around line 140-169: The test fixture function prepareAlternativePlanFixture
relies on the default of the session variable
tidb_opt_enable_alternative_logical_plans, making recorded plans flaky; set the
session variable explicitly at the start of prepareAlternativePlanFixture using
the testkit (e.g. call tk.MustExec to set session
tidb_opt_enable_alternative_logical_plans=ON or =1) so the alternative-plan
behavior is enabled for the entire fixture setup and subsequent plan generation;
also consider restoring the prior value or setting it per-test if other tests
expect a different default.

In `@pkg/planner/core/integration_test.go`:
- Around line 1772-1795: Before asserting plans in
TestCorrelatedSubqueryChoosesLowerCostApplyPlan and the two nearby tests, enable
the alternative-plans feature by setting the session variable
tidb_opt_enable_alternative_logical_plans to ON; specifically, add a session set
(e.g. via tk.MustExec("set session
tidb_opt_enable_alternative_logical_plans=1")) early in the test body (before
building hintedSQL/defaultExplainSQL and running explain/select checks) and
apply the same addition to the other two tests around lines 1799-1900 so the
assertions exercise the intended alternative-plan behavior.

---

Nitpick comments:
In `@pkg/planner/core/optimizer.go`:
- Around line 1005-1013: Replace the fragile string literal check rule.Name() ==
"decorrelate" in the Optimize loop with a stable identifier: either compare
against a package-level constant (e.g., rule.DecorrelateRuleName) defined next
to DecorrelateSolver.Name(), or perform a type assertion/check against the
concrete rule type (e.g., _, ok := rule.(*DecorrelateSolver)) to detect the
decorrelate rule; update both occurrences (the one after rule.Optimize and the
later similar check) to use the chosen approach so the logic no longer depends
on a magic string.

In `@pkg/planner/optimize.go`:
- Around line 477-492: The function buildAndOptimizeLogicalPlanRound currently
returns six separate values (base.Plan, types.NameSlice, two/three booleans, and
error); refactor by introducing a small result struct (e.g.,
LogicalPlanRoundResult) that groups these outputs as named fields (Plan
base.Plan, Names types.NameSlice, NonLogical bool, DecorrelateChanged bool,
FoundEquivalentOuterDrivenIndexJoin bool, Err error), update the
buildAndOptimizeLogicalPlanRound signature to return that struct (and remove the
multi-value return), and update all call sites to use the struct fields instead
of positional returns; ensure you also update any documentation/comments and
preserve existing semantics and error handling when populating and returning the
new struct from buildAndOptimizeLogicalPlanRound.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f0204706-4db3-444f-9152-c37d15012af6

📥 Commits

Reviewing files that changed from the base of the PR and between e6ae2ff and b660922.

📒 Files selected for processing (13)
  • pkg/planner/core/casetest/correlated/correlated_test.go
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_out.json
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_xut.json
  • pkg/planner/core/exhaust_physical_plans.go
  • pkg/planner/core/integration_test.go
  • pkg/planner/core/operator/logicalop/logical_join.go
  • pkg/planner/core/operator/physicalop/physical_index_join.go
  • pkg/planner/core/optimizer.go
  • pkg/planner/core/rule_decorrelate.go
  • pkg/planner/core/task.go
  • pkg/planner/optimize.go
  • pkg/sessionctx/stmtctx/stmtctx.go
  • pkg/sessionctx/stmtctx/stmtctx_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_xut.json
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_out.json

Comment thread pkg/planner/core/casetest/correlated/correlated_test.go Outdated
Comment thread pkg/planner/core/integration_test.go Outdated
@AilinKid
Copy link
Copy Markdown
Contributor Author

I reran the planner micro-benchmark after the latest tuning, against the latest origin/master.

Setup:

  • before: origin/master@0763fed8ff
  • after: current PR HEAD 4eb94c405c
  • command: env GOCACHE=/tmp/go-build GOTMPDIR=/tmp go test ./pkg/planner/core -run '^$' -bench BenchmarkAlternativeLogicalPlanFocused -benchmem -benchtime=1s -count=5
  • failpoints were disabled for the benchmark run
  • numbers below are medians across 5 runs

Median ns/op:

  • CorrelatedScalarSubquery: 130972 -> 192917 (+47.3%)
  • CorrelatedExists: 76040 -> 122740 (+61.4%)
  • ScalarExists: 65184 -> 116454 (+78.7%)
  • ScalarNotExists: 64953 -> 116230 (+79.0%)

Median allocs/op:

  • CorrelatedScalarSubquery: 3603 -> 5298
  • CorrelatedExists: 1878 -> 3028
  • ScalarExists: 1623 -> 2876
  • ScalarNotExists: 1625 -> 2880

I also rechecked the existing failpoint regressions separately: CorrelatedExists, ScalarExists, and ScalarNotExists do not trigger the extra logical round on the current code path. So the remaining overhead here is not from actually entering round 2 for those cases.

@AilinKid
Copy link
Copy Markdown
Contributor Author

I benchmarked two scenarios against the latest origin/master.

  1. A no-second-round EXISTS case: both master and this PR keep the same semi-join shape (Sort -> HashJoin).
  • Optimize: 92.4us -> 147.7us
  • Execute: 338.6us -> 409.5us
  1. The anonymized loaded-stats case that actually triggers the alternative round:
  • master chooses a decorrelated TopN -> HashJoin(semi join) plan
  • this PR chooses Limit -> Apply -> MergeJoin

With 200k rows in each fixture table and only 50 qualifying outer rows, execution improved from 33.45ms to 8.69ms (~3.85x faster, -74.0%), while optimize time increased from 302.3us to 613.5us.

So the tradeoff is now clearer:

  • for cases that do not need the extra round, the PR mainly adds optimizer overhead
  • for the target case where the extra round finds a better non-decorrelated Apply, the execution win is much larger than the additional optimize cost

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/planner/optimize.go (1)

542-543: ⚠️ Potential issue | 🟡 Minor

Capture order drops winner-round unused-view warnings.

bestLogicalPlanCtx is captured before deferred builder.HandleUnusedViewHints() runs. On Line 699 restore, winner warnings from that deferred call are discarded.

Suggested fix direction
-	defer builder.HandleUnusedViewHints()
+	handledUnusedViewHints := false
+	defer func() {
+		if !handledUnusedViewHints {
+			builder.HandleUnusedViewHints()
+		}
+	}()
...
 	if *bestPlan == nil || cost < *bestCost {
 		*bestCost = cost
 		*bestPlan = finalPlan
 		*bestNames = names
+		builder.HandleUnusedViewHints()
+		handledUnusedViewHints = true
 		if bestLogicalPlanCtx != nil {
 			*bestLogicalPlanCtx = saveLogicalPlanBuildCtx(sctx.GetSessionVars())
 		}
 	}

Also applies to: 596-599, 699-699

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

In `@pkg/planner/optimize.go` around lines 542 - 543, The deferred call to
builder.HandleUnusedViewHints() runs after bestLogicalPlanCtx is captured,
causing winner-round unused-view warnings to be lost; change the flow so
HandleUnusedViewHints() executes (and its results are preserved) before
capturing bestLogicalPlanCtx — either remove/defer and call
builder.HandleUnusedViewHints() explicitly at the appropriate point (prior to
assigning bestLogicalPlanCtx) or invoke the deferred function immediately and
capture its returned warnings into a variable that is then merged into
bestLogicalPlanCtx; update all similar sites (the other occurrences around the
builder and restore/restoreWinner logic) to ensure HandleUnusedViewHints runs
before bestLogicalPlanCtx is set.
♻️ Duplicate comments (2)
pkg/planner/core/casetest/correlated/correlated_test.go (1)

111-113: ⚠️ Potential issue | 🟠 Major

Set tidb_opt_enable_alternative_logical_plans explicitly before asserting Apply plans.

Line 111 and Line 141 tests assert automatic Apply-plan selection, but the feature is opt-in. Relying on session defaults makes this regression brittle across environments/runs.

Suggested fix
 func TestCorrelatedSubqueryChoosesLowerCostApplyPlan(t *testing.T) {
 	testkit.RunTestUnderCascades(t, func(t *testing.T, tk *testkit.TestKit, cascades, caller string) {
 		tk.MustExec("use test")
+		tk.MustExec("set @@session.tidb_opt_enable_alternative_logical_plans = 1")
 		tk.MustExec("drop table if exists t1, t2")
 		tk.MustExec("create table t1(a int primary key)")
 		tk.MustExec("create table t2(a int, b int, key idx_a(a))")
@@
 func prepareAlternativePlanFixture(t *testing.T, tk *testkit.TestKit, dom *domain.Domain) {
 	t.Helper()
+	tk.MustExec("set @@session.tidb_opt_enable_alternative_logical_plans = 1")
 
 	tk.MustExec("drop database if exists fixture_db")
 	tk.MustExec("create database fixture_db")

Based on learnings: Applies to **/*_test.go — “Keep test changes minimal and deterministic; avoid broad golden/testdata churn unless required.”

Also applies to: 141-147, 182-188

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

In `@pkg/planner/core/casetest/correlated/correlated_test.go` around lines 111 -
113, The tests rely on the opt-in feature
tidb_opt_enable_alternative_logical_plans but never enable it, making Apply-plan
assertions flaky; update the test body executed by
RunTestUnderCascadesWithDomain to explicitly set the session variable on the
TestKit (tk) before calling prepareAlternativePlanFixture and before any
Apply-plan assertions (e.g., use tk.MustExec("set
@@session.tidb_opt_enable_alternative_logical_plans = 1") or the TestKit API
your suite uses), and mirror this change for the other blocks referencing Apply
assertions (the blocks around prepareAlternativePlanFixture and the later
assertion groups) so the tests are deterministic.
pkg/planner/core/integration_test.go (1)

1771-1799: ⚠️ Potential issue | 🟠 Major

Enable alternative-logical-plan switch in these tests.

Both tests validate “skip extra alternative round” behavior, but they never turn on tidb_opt_enable_alternative_logical_plans. As written, they can pass vacuously when the feature is disabled.

Suggested fix
 func TestSimpleExistsSkipsAlternativeRoundWhenDecorrelatedIndexJoinExists(t *testing.T) {
 	testkit.RunTestUnderCascades(t, func(t *testing.T, tk *testkit.TestKit, cascades, caller string) {
 		tk.MustExec("use test")
+		tk.MustExec("set @@session.tidb_opt_enable_alternative_logical_plans = 1")
 		tk.MustExec("drop table if exists t1, t2")
 func TestScalarExistsSkipsAlternativeRoundWhenDecorrelatedIndexJoinExists(t *testing.T) {
 	testkit.RunTestUnderCascades(t, func(t *testing.T, tk *testkit.TestKit, cascades, caller string) {
 		tk.MustExec("use test")
+		tk.MustExec("set @@session.tidb_opt_enable_alternative_logical_plans = 1")
 		tk.MustExec("drop table if exists t1, t2")

Also applies to: 1801-1874

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

In `@pkg/planner/core/integration_test.go` around lines 1771 - 1799, The tests
TestSimpleExistsSkipsAlternativeRoundWhenDecorrelatedIndexJoinExists (and the
other test in the 1801-1874 range) never enable the alternative-logical-plan
session switch, so they may pass vacuously; update each test to set
tidb_opt_enable_alternative_logical_plans=1 at the start of the test session
(use the TestKit instance tk, e.g. tk.MustExec to set the session variable) and
restore it (or set it back to 0) in a defer if necessary so the rest of the
suite is unaffected; place the tk.MustExec call before creating tables / running
the EXPLAIN and before enabling the failpoint referenced by
failIfAlternativeLogicalPlanRoundTriggered to ensure the alternative-plan path
is active.
🤖 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/stmtctx/stmtctx.go`:
- Around line 744-752: PrepareFreshOptimizeState currently clears many fields
but omits resetting the PlanCacheTracker and RangeFallbackHandler, allowing
plan-cache flags/reasons to leak across Optimize calls; update
StatementContext.PrepareFreshOptimizeState to reinitialize or reset the
PlanCacheTracker and RangeFallbackHandler the same way Reset does (e.g., call
the same reset/reinit logic used in Reset or set sc.PlanCacheTracker and
sc.RangeFallbackHandler to fresh instances/zero values) so that PlanCacheTracker
and RangeFallbackHandler state do not persist between fresh optimize baselines.

---

Outside diff comments:
In `@pkg/planner/optimize.go`:
- Around line 542-543: The deferred call to builder.HandleUnusedViewHints() runs
after bestLogicalPlanCtx is captured, causing winner-round unused-view warnings
to be lost; change the flow so HandleUnusedViewHints() executes (and its results
are preserved) before capturing bestLogicalPlanCtx — either remove/defer and
call builder.HandleUnusedViewHints() explicitly at the appropriate point (prior
to assigning bestLogicalPlanCtx) or invoke the deferred function immediately and
capture its returned warnings into a variable that is then merged into
bestLogicalPlanCtx; update all similar sites (the other occurrences around the
builder and restore/restoreWinner logic) to ensure HandleUnusedViewHints runs
before bestLogicalPlanCtx is set.

---

Duplicate comments:
In `@pkg/planner/core/casetest/correlated/correlated_test.go`:
- Around line 111-113: The tests rely on the opt-in feature
tidb_opt_enable_alternative_logical_plans but never enable it, making Apply-plan
assertions flaky; update the test body executed by
RunTestUnderCascadesWithDomain to explicitly set the session variable on the
TestKit (tk) before calling prepareAlternativePlanFixture and before any
Apply-plan assertions (e.g., use tk.MustExec("set
@@session.tidb_opt_enable_alternative_logical_plans = 1") or the TestKit API
your suite uses), and mirror this change for the other blocks referencing Apply
assertions (the blocks around prepareAlternativePlanFixture and the later
assertion groups) so the tests are deterministic.

In `@pkg/planner/core/integration_test.go`:
- Around line 1771-1799: The tests
TestSimpleExistsSkipsAlternativeRoundWhenDecorrelatedIndexJoinExists (and the
other test in the 1801-1874 range) never enable the alternative-logical-plan
session switch, so they may pass vacuously; update each test to set
tidb_opt_enable_alternative_logical_plans=1 at the start of the test session
(use the TestKit instance tk, e.g. tk.MustExec to set the session variable) and
restore it (or set it back to 0) in a defer if necessary so the rest of the
suite is unaffected; place the tk.MustExec call before creating tables / running
the EXPLAIN and before enabling the failpoint referenced by
failIfAlternativeLogicalPlanRoundTriggered to ensure the alternative-plan path
is active.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b397757d-f122-4bae-9997-2adc2780b562

📥 Commits

Reviewing files that changed from the base of the PR and between b660922 and 1aaad47.

📒 Files selected for processing (10)
  • pkg/planner/core/casetest/correlated/correlated_test.go
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_in.json
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_out.json
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_xut.json
  • pkg/planner/core/exhaust_physical_plans.go
  • pkg/planner/core/integration_test.go
  • pkg/planner/core/physical_plan_test.go
  • pkg/planner/optimize.go
  • pkg/sessionctx/stmtctx/stmtctx.go
  • pkg/sessionctx/stmtctx/stmtctx_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_in.json
  • pkg/planner/core/casetest/correlated/testdata/correlated_subquery_suite_out.json
  • pkg/planner/core/exhaust_physical_plans.go

Comment thread pkg/sessionctx/stmtctx/stmtctx.go Outdated
@ti-chi-bot ti-chi-bot Bot removed the approved label Mar 24, 2026
@AilinKid
Copy link
Copy Markdown
Contributor Author

Addressed the latest CodeRabbit follow-ups in 6148c5c:

  • reset PlanCacheTracker / RangeFallbackHandler in PrepareFreshOptimizeState, and extended TestPrepareFreshOptimizeState to cover the fresh-baseline behavior
  • fixed HandleUnusedViewHints() timing so warnings are captured from the winning round instead of being emitted per round, and added TestOptimizeAlternativeRoundDoesNotDuplicateUnusedViewWarnings
  • replaced the rule.Name() == "decorrelate" string check with the shared decorrelateRuleName constant
  • documented the optimizer CPU tradeoff in the tidb_opt_enable_alternative_logical_plans comment
  • also grouped buildAndOptimizeLogicalPlanRound outputs into a small result struct while touching the same area

I kept the round-selection design itself unchanged for now; the current roundWithoutDecorrelate named const is still sufficient until we actually add more rounds.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/planner/optimize.go (1)

472-480: ⚠️ Potential issue | 🟠 Major

Baseline snapshot copies slice/map references instead of cloning.

mapScalarSubQ (slice) and mapHashCode2UniqueID (map) are copied by reference, not cloned. When round 0 modifies these collections, the baseline copy is also modified since they share the same underlying data. This defeats the purpose of baseline snapshotting for multi-round optimization.

Compare with CaptureLogicalPlanBuildState() (context snippet 3) which properly uses maps.Clone() for its map fields.

🐛 Proposed fix to clone the collections
 func saveLogicalPlanBuildBaseline(sessVars *variable.SessionVars) logicalPlanBuildBaseline {
 	return logicalPlanBuildBaseline{
 		stmtCtxState:             sessVars.StmtCtx.SaveLogicalPlanBuildBaseline(),
 		plannerSelectBlockAsName: sessVars.PlannerSelectBlockAsName.Load(),
-		mapScalarSubQ:            sessVars.MapScalarSubQ,
-		mapHashCode2UniqueID:     sessVars.MapHashCode2UniqueID4ExtendedCol,
+		mapScalarSubQ:            slices.Clone(sessVars.MapScalarSubQ),
+		mapHashCode2UniqueID:     maps.Clone(sessVars.MapHashCode2UniqueID4ExtendedCol),
 		rewritePhaseInfo:         sessVars.RewritePhaseInfo,
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/optimize.go` around lines 472 - 480, saveLogicalPlanBuildBaseline
currently copies the slice/map references into the logicalPlanBuildBaseline
causing shared mutation; modify saveLogicalPlanBuildBaseline so it deep-clones
the collections before returning (clone mapScalarSubQ slice into a new slice and
clone mapHashCode2UniqueID into a new map using the same approach as
CaptureLogicalPlanBuildState(), e.g., using maps.Clone for maps and an explicit
copy for slices), ensuring the returned logicalPlanBuildBaseline has independent
copies of those fields.
🤖 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/optimize.go`:
- Around line 462-470: saveLogicalPlanBuildCtx currently assigns
sessVars.MapScalarSubQ and sessVars.MapHashCode2UniqueID4ExtendedCol by
reference into the returned logicalPlanBuildCtx, which can lead to
shared-mutation bugs; update saveLogicalPlanBuildCtx to deep-copy those maps
(create new maps and copy entries) when populating mapScalarSubQ and
mapHashCode2UniqueID so the logicalPlanBuildCtx owns independent copies, leaving
other fields (stmtCtxState via CaptureLogicalPlanBuildState,
plannerSelectBlockAsName, rewritePhaseInfo) unchanged.

---

Outside diff comments:
In `@pkg/planner/optimize.go`:
- Around line 472-480: saveLogicalPlanBuildBaseline currently copies the
slice/map references into the logicalPlanBuildBaseline causing shared mutation;
modify saveLogicalPlanBuildBaseline so it deep-clones the collections before
returning (clone mapScalarSubQ slice into a new slice and clone
mapHashCode2UniqueID into a new map using the same approach as
CaptureLogicalPlanBuildState(), e.g., using maps.Clone for maps and an explicit
copy for slices), ensuring the returned logicalPlanBuildBaseline has independent
copies of those fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fb34eb1f-fde5-4ce5-b8b0-2f06fc3715ab

📥 Commits

Reviewing files that changed from the base of the PR and between c5e0f33 and 6148c5c.

📒 Files selected for processing (7)
  • pkg/planner/core/optimizer.go
  • pkg/planner/core/physical_plan_test.go
  • pkg/planner/core/rule_decorrelate.go
  • pkg/planner/optimize.go
  • pkg/sessionctx/stmtctx/stmtctx.go
  • pkg/sessionctx/stmtctx/stmtctx_test.go
  • pkg/sessionctx/vardef/tidb_vars.go
✅ Files skipped from review due to trivial changes (3)
  • pkg/planner/core/physical_plan_test.go
  • pkg/sessionctx/stmtctx/stmtctx_test.go
  • pkg/sessionctx/stmtctx/stmtctx.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/sessionctx/vardef/tidb_vars.go
  • pkg/planner/core/rule_decorrelate.go

Comment thread pkg/planner/optimize.go Outdated
Comment on lines +462 to +470
func saveLogicalPlanBuildCtx(sessVars *variable.SessionVars) logicalPlanBuildCtx {
return logicalPlanBuildCtx{
stmtCtxState: sessVars.StmtCtx.SaveLogicalPlanBuildState(),
stmtCtxState: sessVars.StmtCtx.CaptureLogicalPlanBuildState(),
plannerSelectBlockAsName: sessVars.PlannerSelectBlockAsName.Load(),
mapScalarSubQ: sessVars.MapScalarSubQ,
mapHashCode2UniqueID: sessVars.MapHashCode2UniqueID4ExtendedCol,
rewritePhaseInfo: sessVars.RewritePhaseInfo,
}
}
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 | 🟠 Major

Same reference-copy issue exists in saveLogicalPlanBuildCtx.

This function also copies mapScalarSubQ and mapHashCode2UniqueID by reference. While CaptureLogicalPlanBuildState() properly clones its maps, the session-level collections are not cloned here.

🐛 Proposed fix
 func saveLogicalPlanBuildCtx(sessVars *variable.SessionVars) logicalPlanBuildCtx {
 	return logicalPlanBuildCtx{
 		stmtCtxState:             sessVars.StmtCtx.CaptureLogicalPlanBuildState(),
 		plannerSelectBlockAsName: sessVars.PlannerSelectBlockAsName.Load(),
-		mapScalarSubQ:            sessVars.MapScalarSubQ,
-		mapHashCode2UniqueID:     sessVars.MapHashCode2UniqueID4ExtendedCol,
+		mapScalarSubQ:            slices.Clone(sessVars.MapScalarSubQ),
+		mapHashCode2UniqueID:     maps.Clone(sessVars.MapHashCode2UniqueID4ExtendedCol),
 		rewritePhaseInfo:         sessVars.RewritePhaseInfo,
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/optimize.go` around lines 462 - 470, saveLogicalPlanBuildCtx
currently assigns sessVars.MapScalarSubQ and
sessVars.MapHashCode2UniqueID4ExtendedCol by reference into the returned
logicalPlanBuildCtx, which can lead to shared-mutation bugs; update
saveLogicalPlanBuildCtx to deep-copy those maps (create new maps and copy
entries) when populating mapScalarSubQ and mapHashCode2UniqueID so the
logicalPlanBuildCtx owns independent copies, leaving other fields (stmtCtxState
via CaptureLogicalPlanBuildState, plannerSelectBlockAsName, rewritePhaseInfo)
unchanged.

@AilinKid
Copy link
Copy Markdown
Contributor Author

/retest-required

1 similar comment
@AilinKid
Copy link
Copy Markdown
Contributor Author

/retest-required

@ti-chi-bot ti-chi-bot Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Mar 24, 2026
@AilinKid
Copy link
Copy Markdown
Contributor Author

/test tidb_parser_test

@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented Mar 25, 2026

@AilinKid: The specified target(s) for /test were not found.
The following commands are available to trigger required jobs:

/test build
/test check-dev
/test check-dev2
/test mysql-test
/test pull-br-integration-test
/test pull-build-next-gen
/test pull-integration-e2e-test
/test pull-integration-realcluster-test-next-gen
/test pull-lightning-integration-test
/test pull-mysql-client-test
/test pull-mysql-client-test-next-gen
/test pull-unit-test-ddlv1
/test pull-unit-test-next-gen
/test unit-test

The following commands are available to trigger optional jobs:

/test pingcap/tidb/canary_ghpr_unit_test
/test pull-br-integration-test-next-gen
/test pull-check-deps
/test pull-common-test
/test pull-e2e-test
/test pull-error-log-review
/test pull-integration-common-test
/test pull-integration-copr-test
/test pull-integration-ddl-test
/test pull-integration-ddl-test-next-gen
/test pull-integration-e2e-test-next-gen
/test pull-integration-jdbc-test
/test pull-integration-mysql-test
/test pull-integration-nodejs-test
/test pull-integration-python-orm-test
/test pull-mysql-test-next-gen
/test pull-sqllogic-test
/test pull-tiflash-integration-test

Use /test all to run the following jobs that were automatically triggered:

pingcap/tidb/ghpr_build
pingcap/tidb/ghpr_check
pingcap/tidb/ghpr_check2
pingcap/tidb/ghpr_mysql_test
pingcap/tidb/ghpr_unit_test
pingcap/tidb/pull_build_next_gen
pingcap/tidb/pull_integration_e2e_test
pingcap/tidb/pull_integration_realcluster_test_next_gen
pingcap/tidb/pull_mysql_client_test
pingcap/tidb/pull_mysql_client_test_next_gen
pingcap/tidb/pull_unit_test_next_gen
pull-check-deps
pull-error-log-review
Details

In response to this:

/test tidb_parser_test

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.

@ti-chi-bot ti-chi-bot Bot added lgtm and removed needs-1-more-lgtm Indicates a PR needs 1 more LGTM. labels Mar 25, 2026
@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented Mar 25, 2026

[LGTM Timeline notifier]

Timeline:

  • 2026-03-13 09:55:11.647852717 +0000 UTC m=+368.648626163: ☑️ agreed by guo-shaoge.
  • 2026-03-25 02:40:54.376928138 +0000 UTC m=+322450.412998398: ☑️ agreed by qw4990.

Copy link
Copy Markdown

@yudongusa yudongusa left a comment

Choose a reason for hiding this comment

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

Please open a document PR to track

@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented Mar 25, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: guo-shaoge, qw4990, yudongusa

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

The pull request process is described 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

@ti-chi-bot ti-chi-bot Bot added the approved label Mar 25, 2026
@AilinKid
Copy link
Copy Markdown
Contributor Author

/test-required

@AilinKid
Copy link
Copy Markdown
Contributor Author

/retest-required

@ti-chi-bot ti-chi-bot Bot merged commit 0c4df2e into pingcap:master Mar 25, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved lgtm ok-to-test Indicates a PR is ready to be tested. release-note-none Denotes a PR that doesn't merit a release note. sig/planner SIG: Planner size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants