Skip to content

planner/core: discourage degenerate index joins when probe rows approach a full scan | tidb-test=pr/2738 - #67646

Merged
ti-chi-bot[bot] merged 28 commits into
pingcap:masterfrom
guo-shaoge:refine_cost_indexjoin_hashjoin
May 14, 2026
Merged

planner/core: discourage degenerate index joins when probe rows approach a full scan | tidb-test=pr/2738#67646
ti-chi-bot[bot] merged 28 commits into
pingcap:masterfrom
guo-shaoge:refine_cost_indexjoin_hashjoin

Conversation

@guo-shaoge

@guo-shaoge guo-shaoge commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #67610

Problem Summary: When IndexJoin or IndexHashJoin is estimated to probe a large number of inner rows, the optimizer may still prefer it over HashJoin, even though the index join has already degenerated into a near full-scan pattern and can be slower in practice.

What changed and how does it work?

This PR introduces a new optimizer sysvar:

  • tidb_opt_index_join_scan_ratio_threshold

For IndexJoin / IndexHashJoin, the optimizer now evaluates the following ratio:

(buildRows * probeRowsOne) / innerFullScanRows

Where:

  • buildRows: estimated outer-side rows
  • probeRowsOne: estimated inner rows probed per outer row
  • innerFullScanRows: estimated inner-side full scan rows

If the ratio is greater than or equal to tidb_opt_index_join_scan_ratio_threshold, the optimizer adds a large penalty to the index join plan, making it less likely to win against HashJoin.

This helps avoid plans where the total probe work of an index join is already close to a full scan, which is a common shape for degenerate IndexHashJoin / IndexJoin choices.

The change is disabled by default with 0, so it is backward compatible unless explicitly enabled.

This PR also fixes a panic in the new cost path when full-scan stats cannot be derived from the probe plan tree.

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

    • Added new optimizer setting tidb_opt_index_join_max_scan_rows_ratio to tune index-join selection.
  • Improvements

    • Index-join planning gains a probe-vs-build scan-ratio heuristic to prune likely suboptimal index joins and respects forced hints to avoid unsafe pruning.
  • Bug Fixes

    • Fixed null-handling in physical-plan statistics lookup that could cause panics.
  • Tests

    • Added regression tests validating join-plan selection under different scan-ratio settings.

Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
@ti-chi-bot ti-chi-bot Bot added release-note-none Denotes a PR that doesn't merit a release note. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. sig/planner SIG: Planner labels Apr 9, 2026
@pantheon-ai

pantheon-ai Bot commented Apr 9, 2026

Copy link
Copy Markdown

@guo-shaoge I've received your pull request and will start the review. I'll conduct a thorough review covering code quality, potential issues, and implementation details.

⏳ This process typically takes 10-30 minutes depending on the complexity of the changes.

ℹ️ Learn more details on Pantheon AI.

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown

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 scan-ratio pruning heuristic for IndexJoin enumeration controlled by a new sysvar tidb_opt_index_join_max_scan_rows_ratio, wires it into planner enumeration, hardens a stats utility, tweaks cost formatting, and adds regression/unit tests exercising the behavior.

Changes

IndexJoin Scan-Ratio Pruning & Tests

Layer / File(s) Summary
Config / Defaults
pkg/sessionctx/vardef/tidb_vars.go
Add TiDBOptIndexJoinMaxScanRowsRatio and DefOptIndexJoinMaxScanRowsRatio = 0.0.
Session State / Sysvar
pkg/sessionctx/variable/session.go, pkg/sessionctx/variable/sysvar.go
Add SessionVars.IndexJoinMaxScanRowsRatio float64; initialize from default in NewSessionVars; register tidb_opt_index_join_max_scan_rows_ratio sysvar with parsing and setter storing into SessionVars.IndexJoinMaxScanRowsRatio.
Planner Enumeration (Prune Decision)
pkg/planner/core/exhaust_physical_plans.go
Introduce scan-ratio pruning: add indexJoinPruneMinProbeRows/indexJoinPruneMinBuildRows constants, getProbeFullScanRowsForIndexJoinPrune and shouldPruneIndexJoinByScanRatio; make enumerateIndexJoinByOuterIdx null-safe and accept enableRatioPrune; wire enableRatioPrune through tryToEnumerateIndexJoin and exhaustPhysicalPlans4LogicalJoin; add hasForceIndexJoinFamilyHint gating.
Testcases / Fixtures
pkg/planner/core/casetest/cbotest/cbo_test.go, .../testdata/analyze_suite_in.json, .../analyze_suite_out.json, .../analyze_suite_xut.json, pkg/planner/core/plan_cost_ver2_test.go
Add TestReproHashJoinIssue test cases and a regression block that loads precomputed stats for t_small/t_big, toggles tidb_opt_index_join_max_scan_rows_ratio, and asserts plan selection; add unit sub-test for index-join scan-ratio threshold.
Utility Robustness
pkg/planner/core/operator/physicalop/physical_utils.go
GetTblStats now returns nil when copTaskPlan == nil and guards recursion on Children() length.
Cost Formatting (no semantic change)
pkg/planner/core/plan_cost_ver2.go
Reformat getIndexJoinCostVer24PhysicalIndexJoin cost assignment into a multi-line costusage.SumCostVer2(...) expression (semantically unchanged).
sequenceDiagram
    participant TestRunner as Test Runner
    participant Session as SessionVars/Sysvar
    participant Planner as Optimizer (Enumerator & Cost)
    participant Stats as Table Stats (HistColl)

    TestRunner->>Session: set tidb_cost_model_version, set tidb_opt_index_join_max_scan_rows_ratio
    TestRunner->>Stats: load precomputed stats for t_small / t_big
    TestRunner->>Planner: request EXPLAIN (STRAIGHT_JOIN query)
    Planner->>Session: read IndexJoinMaxScanRowsRatio
    Planner->>Stats: fetch probe / full-scan row estimates
    Planner->>Planner: shouldPruneIndexJoinByScanRatio(...) -> prune or enumerate IndexJoin
    Planner->>Planner: compute IndexJoin / HashJoin costs
    Planner->>TestRunner: return chosen plan
    TestRunner->>TestRunner: assert plan matches expected
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • pingcap/tidb#66786: Modifies index-join/apply cost and enumeration logic; likely related.
  • pingcap/tidb#66995: Also touches index-join enumeration and planner decisions; likely related.

Suggested labels

ok-to-test, approved, lgtm

Suggested reviewers

  • terry1purcell
  • qw4990

Poem

🐰 I hopped through histograms and flags,

counted probes across the grass.
A ratio set to prune the race,
now joins pick a fitter pace. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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
Description check ✅ Passed The description follows the template with issue number, problem summary, explanation of changes, completed checklist items, and release notes section.
Linked Issues check ✅ Passed The PR addresses issue #67610 by implementing a scan ratio heuristic to penalize index joins when probe work approaches full scan, with test coverage for the new sysvar behavior.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the index-join scan ratio pruning feature and fixing related panic in GetTblStats; no unrelated modifications detected.
Title check ✅ Passed The title clearly and specifically describes the main change: introducing a planner optimization to discourage degenerate index joins when probe rows approach a full scan.

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

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@guo-shaoge
guo-shaoge requested review from AilinKid and qw4990 April 9, 2026 08:35
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
@codecov

codecov Bot commented Apr 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 53.33333% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.4697%. Comparing base (8ad6083) to head (86ac522).
⚠️ Report is 13 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #67646        +/-   ##
================================================
- Coverage   77.7059%   76.4697%   -1.2363%     
================================================
  Files          1991       1992         +1     
  Lines        552094     563492     +11398     
================================================
+ Hits         429010     430901      +1891     
- Misses       122164     132080      +9916     
+ Partials        920        511       -409     
Flag Coverage Δ
integration 41.5347% <53.3333%> (+1.7329%) ⬆️

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

Components Coverage Δ
dumpling 60.4888% <ø> (ø)
parser ∅ <ø> (∅)
br 49.6115% <ø> (-13.4796%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/casetest/cbotest/cbo_test.go`:
- Around line 173-189: The test sets session variable
tidb_opt_index_join_scan_ratio_threshold to 0.5 inside the
TestReproHashJoinIssue block and never restores it, causing later tests to
inherit the altered scan-ratio; fix this by capturing the original session value
(or the default) before calling tk.MustExec("set
@@session.tidb_opt_index_join_scan_ratio_threshold = 0.5") and restore it after
the repro loop (e.g., with a defer or an explicit tk.MustExec to reset the
variable) so subsequent tests in TestAnalyzeSuiteRegression/IndexJoin revert to
the prior behavior.

In `@pkg/planner/core/plan_cost_ver2_test.go`:
- Around line 557-580: Add a regression test in
pkg/planner/core/plan_cost_ver2_test.go that exercises the no-full-scan-stats
fallback (the branch fixed to avoid a panic when full-scan rows cannot be
derived from the probe tree). Specifically, extend the existing test block that
uses MustExec/MustQuery/Explain format=verbose (the same pattern around the
IndexJoin scan ratio checks) with an additional query variant that produces a
probe tree where full-scan row counts are unavailable (e.g., use a derived table
or a probe with expressions/filters that prevent deriving full-scan rows), set
@@session.tidb_opt_index_join_scan_ratio_threshold to trigger the fallback, run
Explain and assert the plan is produced (no panic) and that the planner picks a
valid join type; use the same helpers (tk.MustExec, tk.MustQuery,
strconv.ParseFloat, require.*) and keep assertions similar to the surrounding
tests so the regression is covered.
🪄 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: ab62511a-c931-472c-886c-a34a8daf9f4c

📥 Commits

Reviewing files that changed from the base of the PR and between de09871 and c62116a.

⛔ Files ignored due to path filters (1)
  • pkg/planner/core/casetest/cbotest/testdata/stats.zip is excluded by !**/*.zip
📒 Files selected for processing (10)
  • pkg/planner/core/casetest/cbotest/cbo_test.go
  • pkg/planner/core/casetest/cbotest/testdata/analyze_suite_in.json
  • pkg/planner/core/casetest/cbotest/testdata/analyze_suite_out.json
  • pkg/planner/core/casetest/cbotest/testdata/analyze_suite_xut.json
  • pkg/planner/core/operator/physicalop/physical_utils.go
  • pkg/planner/core/plan_cost_ver2.go
  • pkg/planner/core/plan_cost_ver2_test.go
  • pkg/sessionctx/vardef/tidb_vars.go
  • pkg/sessionctx/variable/session.go
  • pkg/sessionctx/variable/sysvar.go

Comment thread pkg/planner/core/casetest/cbotest/cbo_test.go
Comment on lines +557 to +580
// Test IndexJoin scan ratio threshold
tk.MustExec("drop table if exists t_outer, t_inner")
tk.MustExec("create table t_outer(a int primary key, b int, key(b))")
tk.MustExec("create table t_inner(a int primary key, b int, key(b))")
tk.MustExec("insert into t_outer values (1,1),(2,2),(3,3),(4,4),(5,5)")
tk.MustExec("insert into t_inner values (1,1),(2,2),(3,3),(4,4),(5,5)")
tk.MustExec("analyze table t_outer, t_inner")
tk.MustExec("set @@session.tidb_opt_hash_join_cost_factor=1")
tk.MustExec("set @@session.tidb_opt_merge_join_cost_factor=100")
tk.MustExec("set @@session.tidb_opt_index_join_cost_factor=1")
tk.MustExec("set @@session.tidb_opt_index_join_scan_ratio_threshold=0")
rs = tk.MustQuery("explain format=verbose select * from t_outer o straight_join t_inner i on o.b = i.b").Rows()
planCost1, err1 = strconv.ParseFloat(rs[0][2].(string), 64)
require.Nil(t, err1)
require.Contains(t, rs[0][0].(string), "IndexJoin")
tk.MustExec("set @@session.tidb_opt_index_join_scan_ratio_threshold=0.8")
rs = tk.MustQuery("explain format=verbose select * from t_outer o straight_join t_inner i on o.b = i.b").Rows()
planCost2, err2 = strconv.ParseFloat(rs[0][2].(string), 64)
require.Nil(t, err2)
require.Less(t, planCost1, planCost2)
require.Contains(t, rs[0][0].(string), "HashJoin")
tk.MustExec("set @@session.tidb_opt_index_join_scan_ratio_threshold=0")
tk.MustExec("set @@session.tidb_opt_hash_join_cost_factor=1")
tk.MustExec("set @@session.tidb_opt_merge_join_cost_factor=1")

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

Add a regression for the no-full-scan-stats fallback.

This covers the analyzed-stats happy path only. The PR also fixes a panic when full-scan rows cannot be derived from the probe tree, but nothing here drives that branch, so the panic fix can regress unnoticed.

Based on learnings, for planner rule or logical/physical plan changes, perform targeted planner unit tests and update rule testdata when needed.

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

In `@pkg/planner/core/plan_cost_ver2_test.go` around lines 557 - 580, Add a
regression test in pkg/planner/core/plan_cost_ver2_test.go that exercises the
no-full-scan-stats fallback (the branch fixed to avoid a panic when full-scan
rows cannot be derived from the probe tree). Specifically, extend the existing
test block that uses MustExec/MustQuery/Explain format=verbose (the same pattern
around the IndexJoin scan ratio checks) with an additional query variant that
produces a probe tree where full-scan row counts are unavailable (e.g., use a
derived table or a probe with expressions/filters that prevent deriving
full-scan rows), set @@session.tidb_opt_index_join_scan_ratio_threshold to
trigger the fallback, run Explain and assert the plan is produced (no panic) and
that the planner picks a valid join type; use the same helpers (tk.MustExec,
tk.MustQuery, strconv.ParseFloat, require.*) and keep assertions similar to the
surrounding tests so the regression is covered.

Comment thread pkg/sessionctx/vardef/tidb_vars.go Outdated
Comment thread pkg/planner/core/plan_cost_ver2.go Outdated

@qw4990 qw4990 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we implement this strategy on the physical optimization stage (like skyline pruning for index selection) instead of the cost model? The cost model is not forced; even we add this large penalty, we can't guarantee the optimizer will choose HashJoin instead of IndexJoin.

guo-shaoge added 2 commits May 7, 2026 17:43
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

542-548: 💤 Low value

Add a brief doc comment explaining the stats semantics

HistColl.RealtimeCount is the total live row count of the underlying table (not the filtered selectivity-adjusted count), which is the right denominator for the scan-ratio check. Future readers unfamiliar with this distinction may mistakenly believe StatsInfo.RowCount (already filtered) was the intent.

📝 Proposed doc comment
+// getProbeFullScanRowsForIndexJoinPrune returns the estimated total row count
+// of the inner (probe) side as if it were a full table scan, by reading
+// HistColl.RealtimeCount from the plan's StatsInfo. This is distinct from
+// StatsInfo.RowCount, which reflects selectivity-adjusted estimates.
+// Returns 0 when reliable statistics are unavailable (safe: prune is skipped).
 func getProbeFullScanRowsForIndexJoinPrune(p base.LogicalPlan) float64 {
🤖 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/exhaust_physical_plans.go` around lines 542 - 548, Add a
short doc comment above getProbeFullScanRowsForIndexJoinPrune explaining that
HistColl.RealtimeCount represents the total live row count of the underlying
table (unfiltered) and is intentionally used as the denominator for scan-ratio
checks, as opposed to StatsInfo.RowCount which is a
selectivity-adjusted/filtered count; mention this distinction so future readers
understand why RealtimeCount is used for the probe full-scan rows calculation.

550-563: 💤 Low value

Add a doc comment for the scan-ratio heuristic

The function encodes a non-obvious heuristic. Per the coding guidelines, a brief comment should explain the intent and constraints so future readers can reason about the pruning conditions without consulting external documents.

📝 Proposed doc comment
+// shouldPruneIndexJoinByScanRatio reports whether an IndexJoin candidate
+// should be pruned because its total estimated probe work approaches a full
+// inner-side scan. It computes (buildRows * probeRowsOne) / innerFullScanRows
+// and prunes when the ratio is >= threshold.
+// Pruning is skipped when threshold is 0 (disabled), buildRows is <= 1
+// (degenerate outer side), or reliable full-scan statistics are unavailable.
 func shouldPruneIndexJoinByScanRatio(threshold, buildRows, probeRowsOne float64, probe base.LogicalPlan) bool {
🤖 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/exhaust_physical_plans.go` around lines 550 - 563, The
function shouldPruneIndexJoinByScanRatio encodes a heuristic determining when to
prune an index join based on the ratio of total probe rows to inner full-scan
rows but lacks documentation; add a concise doc comment above
shouldPruneIndexJoinByScanRatio that explains the intent (prune when estimated
total probe work divided by inner full-scan rows meets/exceeds threshold),
documents parameters (threshold, buildRows, probeRowsOne, and the probe plan
used to compute innerFullScanRows via getProbeFullScanRowsForIndexJoinPrune),
and notes important constraints/early-exit conditions (threshold <= 0, buildRows
<= 1, innerFullScanRows <= 0, or probeRowsTot <= 0 all return false) so future
readers understand why those checks exist.
🤖 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.

Inline comments:
In `@pkg/planner/core/exhaust_physical_plans.go`:
- Around line 516-518: The pruning call to shouldPruneIndexJoinByScanRatio must
be skipped when the user has set a force index-join hint; modify
enumerateIndexJoinByOuterIdx (the place that currently calls
shouldPruneIndexJoinByScanRatio) to first detect a force index-join hint (use
the existing join hint indicator, e.g. p.PreferJoinType or the same flag used by
tryToEnumerateIndexJoin/exhaustPhysicalPlans4LogicalJoin to signal
INL_JOIN/INL_HASH_JOIN) and only call shouldPruneIndexJoinByScanRatio when no
force hint is active, so force hints override the scan-ratio heuristic and avoid
misleading “inapplicable” warnings from recordIndexJoinHintWarnings.

---

Nitpick comments:
In `@pkg/planner/core/exhaust_physical_plans.go`:
- Around line 542-548: Add a short doc comment above
getProbeFullScanRowsForIndexJoinPrune explaining that HistColl.RealtimeCount
represents the total live row count of the underlying table (unfiltered) and is
intentionally used as the denominator for scan-ratio checks, as opposed to
StatsInfo.RowCount which is a selectivity-adjusted/filtered count; mention this
distinction so future readers understand why RealtimeCount is used for the probe
full-scan rows calculation.
- Around line 550-563: The function shouldPruneIndexJoinByScanRatio encodes a
heuristic determining when to prune an index join based on the ratio of total
probe rows to inner full-scan rows but lacks documentation; add a concise doc
comment above shouldPruneIndexJoinByScanRatio that explains the intent (prune
when estimated total probe work divided by inner full-scan rows meets/exceeds
threshold), documents parameters (threshold, buildRows, probeRowsOne, and the
probe plan used to compute innerFullScanRows via
getProbeFullScanRowsForIndexJoinPrune), and notes important
constraints/early-exit conditions (threshold <= 0, buildRows <= 1,
innerFullScanRows <= 0, or probeRowsTot <= 0 all return false) so future readers
understand why those checks exist.
🪄 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: b238bf8b-288f-4e6e-bd65-c832cdb64110

📥 Commits

Reviewing files that changed from the base of the PR and between c62116a and fe9a267.

📒 Files selected for processing (4)
  • pkg/planner/core/casetest/cbotest/cbo_test.go
  • pkg/planner/core/exhaust_physical_plans.go
  • pkg/planner/core/plan_cost_ver2.go
  • pkg/planner/core/plan_cost_ver2_test.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/planner/core/plan_cost_ver2.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/planner/core/casetest/cbotest/cbo_test.go
  • pkg/planner/core/plan_cost_ver2_test.go

Comment thread pkg/planner/core/exhaust_physical_plans.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/core/exhaust_physical_plans.go (1)

516-518: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Force INL hints are still bypassed by early scan-ratio pruning

At Line 516, pruning runs before force-hint handling, so INL_JOIN / INL_HASH_JOIN can still be eliminated and later reported as “inapplicable” even though the real cause is heuristic pruning. Please skip this pruning path when a force index-join hint is present.

Proposed minimal fix
-	if shouldPruneIndexJoinByScanRatio(p.SCtx().GetSessionVars().IndexJoinMaxProbeScanRatio, buildRows, avgInnerRowCnt, p.Children()[1-outerIdx]) {
+	forceIndexJoin := p.PreferAny(
+		h.PreferLeftAsINLJInner, h.PreferRightAsINLJInner,
+		h.PreferLeftAsINLHJInner, h.PreferRightAsINLHJInner,
+		h.PreferLeftAsINLMJInner, h.PreferRightAsINLMJInner,
+	)
+	if !forceIndexJoin &&
+		shouldPruneIndexJoinByScanRatio(p.SCtx().GetSessionVars().IndexJoinMaxProbeScanRatio, buildRows, avgInnerRowCnt, p.Children()[1-outerIdx]) {
 		return nil
 	}
🤖 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/exhaust_physical_plans.go` around lines 516 - 518, The
pruning call to shouldPruneIndexJoinByScanRatio in exhaust_physical_plans.go is
being executed before honoring force index-join hints, causing
INL_JOIN/INL_HASH_JOIN to be incorrectly discarded; fix by skipping that pruning
branch when a forced index-join hint is present for this join node (i.e., detect
INL_JOIN or INL_HASH_JOIN force hint on the current join) and only call
shouldPruneIndexJoinByScanRatio(IndexJoinMaxProbeScanRatio, buildRows,
avgInnerRowCnt, p.Children()[1-outerIdx]) when no such force hint exists; locate
the check around the existing shouldPruneIndexJoinByScanRatio call and gate it
on the presence/absence of the force-index-join hint for this plan node.
🤖 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.

Duplicate comments:
In `@pkg/planner/core/exhaust_physical_plans.go`:
- Around line 516-518: The pruning call to shouldPruneIndexJoinByScanRatio in
exhaust_physical_plans.go is being executed before honoring force index-join
hints, causing INL_JOIN/INL_HASH_JOIN to be incorrectly discarded; fix by
skipping that pruning branch when a forced index-join hint is present for this
join node (i.e., detect INL_JOIN or INL_HASH_JOIN force hint on the current
join) and only call shouldPruneIndexJoinByScanRatio(IndexJoinMaxProbeScanRatio,
buildRows, avgInnerRowCnt, p.Children()[1-outerIdx]) when no such force hint
exists; locate the check around the existing shouldPruneIndexJoinByScanRatio
call and gate it on the presence/absence of the force-index-join hint for this
plan node.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1917532c-3ea8-433a-864b-768fd1248e8a

📥 Commits

Reviewing files that changed from the base of the PR and between fe9a267 and 271b1ee.

📒 Files selected for processing (6)
  • pkg/planner/core/casetest/cbotest/cbo_test.go
  • pkg/planner/core/exhaust_physical_plans.go
  • pkg/planner/core/plan_cost_ver2_test.go
  • pkg/sessionctx/vardef/tidb_vars.go
  • pkg/sessionctx/variable/session.go
  • pkg/sessionctx/variable/sysvar.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/sessionctx/vardef/tidb_vars.go
  • pkg/planner/core/casetest/cbotest/cbo_test.go
  • pkg/planner/core/plan_cost_ver2_test.go

Signed-off-by: guo-shaoge <shaoge1994@163.com>
Comment thread pkg/sessionctx/vardef/tidb_vars.go Outdated
@@ -1480,6 +1461,7 @@ const (
DefOptMergeJoinCostFactor = 1.0
DefOptHashJoinCostFactor = 1.0
DefOptIndexJoinCostFactor = 1.0
DefOptIndexJoinMaxProbeScanRatio = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we set the default value to 1.0 or 1.5? If the IndexJoin is going to scan 100% or 150% rows than HashJoin's FullScan, it seems like there is no reason to choose IndexJoin.

Comment thread pkg/planner/core/exhaust_physical_plans.go Outdated
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.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 12, 2026
…exjoin_hashjoin

Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
@AilinKid

Copy link
Copy Markdown
Contributor

/test pull-unit-test-next-gen

@tiprow

tiprow Bot commented May 13, 2026

Copy link
Copy Markdown

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

/test fast_test_tiprow
/test tidb_parser_test

Use /test all to run all jobs.

Details

In response to this:

/test pull-unit-test-next-gen

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.

@qw4990

qw4990 commented May 13, 2026

Copy link
Copy Markdown
Contributor

/test pull-unit-test-next-gen

@tiprow

tiprow Bot commented May 13, 2026

Copy link
Copy Markdown

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

/test fast_test_tiprow
/test tidb_parser_test

Use /test all to run all jobs.

Details

In response to this:

/test pull-unit-test-next-gen

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.

@terry1purcell

Copy link
Copy Markdown
Contributor

/retest-required

@ti-chi-bot

ti-chi-bot Bot commented May 13, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: AilinKid, qw4990, terry1purcell

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 May 13, 2026
@guo-shaoge

Copy link
Copy Markdown
Contributor Author

/retest

Signed-off-by: guo-shaoge <shaoge1994@163.com>
@guo-shaoge

Copy link
Copy Markdown
Contributor Author

/retest

@ti-chi-bot
ti-chi-bot Bot merged commit e96b621 into pingcap:master May 14, 2026
36 checks passed
@ti-chi-bot ti-chi-bot Bot added the needs-cherry-pick-release-8.5 Should cherry pick this PR to release-8.5 branch. label May 29, 2026
@ti-chi-bot

Copy link
Copy Markdown
Member

In response to a cherrypick label: new pull request created to branch release-8.5: #68756.
But this PR has conflicts, please resolve them!

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

Labels

approved lgtm 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.

planner: inaccurate cost model for HashAgg+IndexJoin vs HashAgg+HashJoin

5 participants