Skip to content

planner: support index_join_first() hint to prefer index join#66596

Open
fixdb wants to merge 1 commit intopingcap:masterfrom
fixdb:indexjoinfirst
Open

planner: support index_join_first() hint to prefer index join#66596
fixdb wants to merge 1 commit intopingcap:masterfrom
fixdb:indexjoinfirst

Conversation

@fixdb
Copy link
Contributor

@fixdb fixdb commented Feb 28, 2026

What problem does this PR solve?

Issue Number: close #61501

Problem Summary:

What changed and how does it work?

support index_join_first() hint to prefer index join

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.

Add  index_join_first() hint to prefer index join.

Summary by CodeRabbit

  • New Features

    • Added INDEX_JOIN_FIRST optimizer hint enabling users to prefer index-based join methods during query optimization across multi-way and complex join scenarios.
  • Tests

    • Added comprehensive test coverage validating the new optimizer hint functionality with various join configurations and hint combinations.

@ti-chi-bot ti-chi-bot bot added do-not-merge/needs-tests-checked release-note Denotes a PR that will be considered when it comes time to generate release notes. labels Feb 28, 2026
@pantheon-ai
Copy link

pantheon-ai bot commented Feb 28, 2026

Review Complete

Findings: 1 issues
Posted: 1
Duplicates/Skipped: 0

@ti-chi-bot ti-chi-bot bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Feb 28, 2026
@ti-chi-bot
Copy link

ti-chi-bot bot commented Feb 28, 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 benmeadowcroft, d3hunter, winoros 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

@tiprow
Copy link

tiprow bot commented Feb 28, 2026

Hi @fixdb. 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.

@ti-chi-bot ti-chi-bot bot added the sig/planner SIG: Planner label Feb 28, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

This pull request introduces support for a new INDEX_JOIN_FIRST optimizer hint that instructs the query planner to prefer index-based joins over other join types when available. The hint is wired through the parser, hint infrastructure, and join optimization logic.

Changes

Cohort / File(s) Summary
Parser Infrastructure
pkg/parser/ast/misc.go, pkg/parser/hintparser.y, pkg/parser/misc.go
Added lexical and syntactic support for the INDEX_JOIN_FIRST hint, including token definition (hintIndexJoinFirst), grammar rule for NullaryHintName, identifier recognition, and hint-to-token mapping.
Hint Definition & Parsing
pkg/util/hint/hint.go
Added HintIndexJoinFirst constant, IndexJoinFirst bool flag to PlanHints struct, and parsing logic to recognize and propagate the hint from statement-level hints.
Join Optimizer Logic
pkg/planner/core/operator/logicalop/logical_join.go, pkg/planner/core/exhaust_physical_plans.go
Integrated hint handling into join type preference and conflict detection; added preferIndexJoinFamily conditional check that prioritizes index-join plans when the hint is active, and updated conflict detection to exclude the soft preference flag.
Join Reordering
pkg/planner/core/rule_join_reorder.go
Introduced indexJoinFirstHints field to basicJoinGroupInfo for propagating the hint through cross-join groups, with logic to thread hints from child joins and re-apply them during join reordering.
Test Coverage
pkg/planner/core/casetest/hint/hint_test.go
Added TestIndexJoinFirstHint function with comprehensive test cases covering two-way and three-way joins, interactions with LEADING and STRAIGHT_JOIN hints, and verification that index-join operators are preferred when the hint is applied.

Sequence Diagram(s)

sequenceDiagram
    participant Parser
    participant HintParser as Hint Parser
    participant Optimizer as Join Optimizer
    participant PhysicalPlan as Physical Plan Gen

    Parser->>HintParser: Parse INDEX_JOIN_FIRST hint
    HintParser->>HintParser: Recognize hintIndexJoinFirst token
    HintParser->>Optimizer: Create PlanHints with IndexJoinFirst=true
    Optimizer->>Optimizer: SetPreferredJoinTypeAndOrder()<br/>set PreferIndexJoinFirst flag
    Optimizer->>PhysicalPlan: exhaust_physical_plans()
    PhysicalPlan->>PhysicalPlan: Check preferIndexJoinFamily()<br/>prioritize index-join variants
    PhysicalPlan-->>Optimizer: Return index-join plans
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A hint for joins so fine and bright,
INDEX_JOIN_FIRST sets things right!
Through parser, planner, plans so deep,
Index joins leap while others sleep.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 title clearly identifies the main change: adding support for the index_join_first() hint to prefer index joins in the planner.
Description check ✅ Passed The description follows the template structure, includes issue number (#61501), problem summary, and release notes. However, the problem summary section is empty and could be more detailed.
Linked Issues check ✅ Passed The PR implements the core requirement from issue #61501: adding a hint (index_join_first) to prefer IndexJoin. Parser changes support hint recognition, planner changes handle the hint during join optimization, and tests validate the functionality.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the index_join_first hint. No unrelated modifications or scope creep detected in the parser, planner, hint system, or test additions.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.5.0)

Command failed


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.

@fixdb
Copy link
Contributor Author

fixdb commented Feb 28, 2026

/ok-to-test

@ti-chi-bot ti-chi-bot bot added the ok-to-test Indicates a PR is ready to be tested. label Feb 28, 2026
@ti-chi-bot
Copy link

ti-chi-bot bot commented Feb 28, 2026

@fixdb: The following test 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/check_dev 400a7b7 link true /test check-dev

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.

@ti-chi-bot
Copy link

ti-chi-bot bot commented Feb 28, 2026

[FORMAT CHECKER NOTIFICATION]

Notice: To remove the do-not-merge/needs-tests-checked label, please finished the tests then check the finished items in description.

For example:

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No code

📖 For more info, you can check the "Contribute Code" section in the development guide.

Copy link

@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/util/hint/hint.go (1)

132-134: Align wording with actual soft-preference semantics.

The comment says “always prefer”, but the implementation and surrounding docs describe a “when possible” preference.

✏️ Suggested wording tweak
-	// HintIndexJoinFirst is a SQL hint to indicate the optimizer should always prefer index join over other join types.
+	// HintIndexJoinFirst is a SQL hint to indicate the optimizer should prefer index join over other join types when possible.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/util/hint/hint.go` around lines 132 - 134, Update the doc comment for the
HintIndexJoinFirst constant to reflect its soft-preference semantics (i.e.,
prefer index join when possible rather than "always prefer"); modify the comment
above HintIndexJoinFirst to say it indicates the optimizer should prefer index
joins when feasible and that it does not force them or take parameters, keeping
the constant name and behavior unchanged so readers understand it is a
best-effort preference.
pkg/planner/core/operator/logicalop/logical_join.go (1)

1710-1715: Align index_join_first assignment with its stated precedence contract

On Line 1713, the code sets PreferIndexJoinFirst unconditionally, while the comment says it should be set only when no side-specific index-join hint is forced. Adding that guard here would keep precedence explicit and less brittle.

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

In `@pkg/planner/core/operator/logicalop/logical_join.go` around lines 1710 -
1715, The assignment of p.PreferJoinType |= utilhint.PreferIndexJoinFirst when
hintInfo.IndexJoinFirst is true should be guarded so it only applies if no
side-specific index-join hint is forced; update the block around
hintInfo.IndexJoinFirst to check that neither left nor right table-specific
index-join force flags are set (e.g., the corresponding hintInfo fields that
indicate a forced index join for a particular side) before setting
p.PreferJoinType, leaving other behavior unchanged.
🤖 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/hint/hint_test.go`:
- Around line 456-521: The test's plan-type matcher only treats "IndexJoin" and
"IndexHashJoin" as valid, causing false failures when plans use
"IndexMergeJoin"; update each check that sets hasIndexJoin by expanding the
condition to also accept "IndexMergeJoin" (look for occurrences of hasIndexJoin
variable and the planType checks in hint_test.go) so the test recognizes all
index-join-family plans as success.

In `@pkg/planner/core/exhaust_physical_plans.go`:
- Around line 2272-2276: The current block in the INDEX_JOIN_FIRST path returns
true for any index-join family before honoring explicit INL_* preferences;
change the logic in the p.PreferAny(h.PreferIndexJoinFirst) branch to first call
getIndexJoinSideAndMethod(physicPlan) to determine isIndexJoin, and only return
true when isIndexJoin is true AND none of the explicit INL preferences are set
(i.e. !p.PreferAny(h.PreferInlJoin, h.PreferInlHashJoin, h.PreferInlMergeJoin));
otherwise fall through so explicit INL_JOIN/INL_HASH_JOIN/INL_MERGE_JOIN
preferences can still decide.

---

Nitpick comments:
In `@pkg/planner/core/operator/logicalop/logical_join.go`:
- Around line 1710-1715: The assignment of p.PreferJoinType |=
utilhint.PreferIndexJoinFirst when hintInfo.IndexJoinFirst is true should be
guarded so it only applies if no side-specific index-join hint is forced; update
the block around hintInfo.IndexJoinFirst to check that neither left nor right
table-specific index-join force flags are set (e.g., the corresponding hintInfo
fields that indicate a forced index join for a particular side) before setting
p.PreferJoinType, leaving other behavior unchanged.

In `@pkg/util/hint/hint.go`:
- Around line 132-134: Update the doc comment for the HintIndexJoinFirst
constant to reflect its soft-preference semantics (i.e., prefer index join when
possible rather than "always prefer"); modify the comment above
HintIndexJoinFirst to say it indicates the optimizer should prefer index joins
when feasible and that it does not force them or take parameters, keeping the
constant name and behavior unchanged so readers understand it is a best-effort
preference.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25ac994 and 400a7b7.

📒 Files selected for processing (9)
  • pkg/parser/ast/misc.go
  • pkg/parser/hintparser.go
  • pkg/parser/hintparser.y
  • pkg/parser/misc.go
  • pkg/planner/core/casetest/hint/hint_test.go
  • pkg/planner/core/exhaust_physical_plans.go
  • pkg/planner/core/operator/logicalop/logical_join.go
  • pkg/planner/core/rule_join_reorder.go
  • pkg/util/hint/hint.go

Comment on lines +456 to +521
// Verify hint is parsed and index join is chosen when an index is available.
rows := testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() */ t1.a, t2.b from t1 join t2 on t1.a = t2.a").Rows()
hasIndexJoin := false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() hint should prefer index join when index is available")

// Verify hint on three-way join picks index join.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() */ t1.a, t2.b, t3.b from t1 join t2 on t1.a = t2.a join t3 on t2.a = t3.a").Rows()
hasIndexJoin = false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() hint should prefer index join in multi-way join when index is available")

// Verify show warnings contains no error when hint is used.
testKit.MustQuery("select /*+ INDEX_JOIN_FIRST() */ t1.a from t1 join t2 on t1.a = t2.a")
warnings := testKit.Session().GetSessionVars().StmtCtx.GetWarnings()
for _, warn := range warnings {
require.NotContains(t, warn.Err.Error(), "is inapplicable", "INDEX_JOIN_FIRST() should not produce inapplicable warnings when index exists")
}

// Verify INDEX_JOIN_FIRST + LEADING: join order is determined by LEADING, join method by INDEX_JOIN_FIRST.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() LEADING(t2, t1) */ t1.a, t2.b from t1 join t2 on t1.a = t2.a").Rows()
hasIndexJoin = false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() should prefer index join even when LEADING hint controls join order")

// Verify INDEX_JOIN_FIRST + LEADING on three-way join.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() LEADING(t1, t2, t3) */ t1.a, t2.b from t1 join t2 on t1.a = t2.a join t3 on t2.a = t3.a").Rows()
hasIndexJoin = false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() should prefer index join alongside LEADING in multi-way join")

// Verify INDEX_JOIN_FIRST + STRAIGHT_JOIN: join order fixed by table order, join method by INDEX_JOIN_FIRST.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() STRAIGHT_JOIN() */ t1.a, t2.b from t1 join t2 on t1.a = t2.a").Rows()
hasIndexJoin = false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() should prefer index join even when STRAIGHT_JOIN() controls join order")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test matcher omits IndexMergeJoin, which can create false failures

The assertions only treat IndexJoin/IndexHashJoin as success. Since the hint targets index-join family preference, plans using IndexMergeJoin would be valid but fail this test.

✅ Suggested test matcher update
+	containsIndexJoinFamily := func(rows [][]interface{}) bool {
+		for _, row := range rows {
+			planType := row[0].(string)
+			if strings.Contains(planType, "IndexJoin") ||
+				strings.Contains(planType, "IndexHashJoin") ||
+				strings.Contains(planType, "IndexMergeJoin") {
+				return true
+			}
+		}
+		return false
+	}
@@
-		hasIndexJoin := false
-		for _, row := range rows {
-			planType := row[0].(string)
-			if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
-				hasIndexJoin = true
-				break
-			}
-		}
+		hasIndexJoin := containsIndexJoinFamily(rows)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Verify hint is parsed and index join is chosen when an index is available.
rows := testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() */ t1.a, t2.b from t1 join t2 on t1.a = t2.a").Rows()
hasIndexJoin := false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() hint should prefer index join when index is available")
// Verify hint on three-way join picks index join.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() */ t1.a, t2.b, t3.b from t1 join t2 on t1.a = t2.a join t3 on t2.a = t3.a").Rows()
hasIndexJoin = false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() hint should prefer index join in multi-way join when index is available")
// Verify show warnings contains no error when hint is used.
testKit.MustQuery("select /*+ INDEX_JOIN_FIRST() */ t1.a from t1 join t2 on t1.a = t2.a")
warnings := testKit.Session().GetSessionVars().StmtCtx.GetWarnings()
for _, warn := range warnings {
require.NotContains(t, warn.Err.Error(), "is inapplicable", "INDEX_JOIN_FIRST() should not produce inapplicable warnings when index exists")
}
// Verify INDEX_JOIN_FIRST + LEADING: join order is determined by LEADING, join method by INDEX_JOIN_FIRST.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() LEADING(t2, t1) */ t1.a, t2.b from t1 join t2 on t1.a = t2.a").Rows()
hasIndexJoin = false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() should prefer index join even when LEADING hint controls join order")
// Verify INDEX_JOIN_FIRST + LEADING on three-way join.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() LEADING(t1, t2, t3) */ t1.a, t2.b from t1 join t2 on t1.a = t2.a join t3 on t2.a = t3.a").Rows()
hasIndexJoin = false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() should prefer index join alongside LEADING in multi-way join")
// Verify INDEX_JOIN_FIRST + STRAIGHT_JOIN: join order fixed by table order, join method by INDEX_JOIN_FIRST.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() STRAIGHT_JOIN() */ t1.a, t2.b from t1 join t2 on t1.a = t2.a").Rows()
hasIndexJoin = false
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") || strings.Contains(planType, "IndexHashJoin") {
hasIndexJoin = true
break
}
}
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() should prefer index join even when STRAIGHT_JOIN() controls join order")
// Verify hint is parsed and index join is chosen when an index is available.
containsIndexJoinFamily := func(rows [][]interface{}) bool {
for _, row := range rows {
planType := row[0].(string)
if strings.Contains(planType, "IndexJoin") ||
strings.Contains(planType, "IndexHashJoin") ||
strings.Contains(planType, "IndexMergeJoin") {
return true
}
}
return false
}
rows := testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() */ t1.a, t2.b from t1 join t2 on t1.a = t2.a").Rows()
hasIndexJoin := containsIndexJoinFamily(rows)
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() hint should prefer index join when index is available")
// Verify hint on three-way join picks index join.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() */ t1.a, t2.b, t3.b from t1 join t2 on t1.a = t2.a join t3 on t2.a = t3.a").Rows()
hasIndexJoin = containsIndexJoinFamily(rows)
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() hint should prefer index join in multi-way join when index is available")
// Verify show warnings contains no error when hint is used.
testKit.MustQuery("select /*+ INDEX_JOIN_FIRST() */ t1.a from t1 join t2 on t1.a = t2.a")
warnings := testKit.Session().GetSessionVars().StmtCtx.GetWarnings()
for _, warn := range warnings {
require.NotContains(t, warn.Err.Error(), "is inapplicable", "INDEX_JOIN_FIRST() should not produce inapplicable warnings when index exists")
}
// Verify INDEX_JOIN_FIRST + LEADING: join order is determined by LEADING, join method by INDEX_JOIN_FIRST.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() LEADING(t2, t1) */ t1.a, t2.b from t1 join t2 on t1.a = t2.a").Rows()
hasIndexJoin = containsIndexJoinFamily(rows)
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() should prefer index join even when LEADING hint controls join order")
// Verify INDEX_JOIN_FIRST + LEADING on three-way join.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() LEADING(t1, t2, t3) */ t1.a, t2.b from t1 join t2 on t1.a = t2.a join t3 on t2.a = t3.a").Rows()
hasIndexJoin = containsIndexJoinFamily(rows)
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() should prefer index join alongside LEADING in multi-way join")
// Verify INDEX_JOIN_FIRST + STRAIGHT_JOIN: join order fixed by table order, join method by INDEX_JOIN_FIRST.
rows = testKit.MustQuery("explain format='brief' select /*+ INDEX_JOIN_FIRST() STRAIGHT_JOIN() */ t1.a, t2.b from t1 join t2 on t1.a = t2.a").Rows()
hasIndexJoin = containsIndexJoinFamily(rows)
require.True(t, hasIndexJoin, "INDEX_JOIN_FIRST() should prefer index join even when STRAIGHT_JOIN() controls join order")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/casetest/hint/hint_test.go` around lines 456 - 521, The
test's plan-type matcher only treats "IndexJoin" and "IndexHashJoin" as valid,
causing false failures when plans use "IndexMergeJoin"; update each check that
sets hasIndexJoin by expanding the condition to also accept "IndexMergeJoin"
(look for occurrences of hasIndexJoin variable and the planType checks in
hint_test.go) so the test recognizes all index-join-family plans as success.

Comment on lines +2272 to +2276
// index_join_first: prefer any index join method on either side.
if p.PreferAny(h.PreferIndexJoinFirst) {
_, _, isIndexJoin := getIndexJoinSideAndMethod(physicPlan)
return isIndexJoin
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

INDEX_JOIN_FIRST currently masks explicit INL_* preferences

On Line 2273, INDEX_JOIN_FIRST returns true for any index-join family plan before the explicit side/method checks run. If users specify both INDEX_JOIN_FIRST() and a specific INL_JOIN/INL_HASH_JOIN/INL_MERGE_JOIN, the explicit preference becomes non-decisive in this path.

💡 Suggested fix
 func preferIndexJoinFamily(lp base.LogicalPlan, physicPlan base.PhysicalPlan) (preferred bool) {
@@
-	// index_join_first: prefer any index join method on either side.
-	if p.PreferAny(h.PreferIndexJoinFirst) {
+	// index_join_first: prefer any index join method on either side,
+	// but do not override explicit INL_* method/side preferences.
+	hasSpecificINLHint := p.PreferAny(
+		h.PreferRightAsINLJInner, h.PreferRightAsINLHJInner, h.PreferRightAsINLMJInner,
+		h.PreferLeftAsINLJInner, h.PreferLeftAsINLHJInner, h.PreferLeftAsINLMJInner,
+	)
+	if p.PreferAny(h.PreferIndexJoinFirst) && !hasSpecificINLHint {
 		_, _, isIndexJoin := getIndexJoinSideAndMethod(physicPlan)
 		return isIndexJoin
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/exhaust_physical_plans.go` around lines 2272 - 2276, The
current block in the INDEX_JOIN_FIRST path returns true for any index-join
family before honoring explicit INL_* preferences; change the logic in the
p.PreferAny(h.PreferIndexJoinFirst) branch to first call
getIndexJoinSideAndMethod(physicPlan) to determine isIndexJoin, and only return
true when isIndexJoin is true AND none of the explicit INL preferences are set
(i.e. !p.PreferAny(h.PreferInlJoin, h.PreferInlHashJoin, h.PreferInlMergeJoin));
otherwise fall through so explicit INL_JOIN/INL_HASH_JOIN/INL_MERGE_JOIN
preferences can still decide.

if physicPlan == nil {
return false
}
// index_join_first: prefer any index join method on either side.
Copy link

Choose a reason for hiding this comment

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

[P2] INDEX_JOIN_FIRST bypasses table-specific INL_* enforcement

Why: When INDEX_JOIN_FIRST() is combined with table-specific INL_JOIN/INL_HASH_JOIN/INL_MERGE_JOIN hints, the optimizer can select an index join plan that violates the explicit INL_* method/inner-side request without warning, potentially causing silent performance regressions.

Evidence: pkg/planner/core/exhaust_physical_plans.go:2272

The preferIndexJoinFamily() function returns true for any index-join-family plan when PreferIndexJoinFirst is set, short-circuiting the more specific INL_* inner-side and method checks. This makes any index join "hint applicable" in the cost-based selection, suppressing inapplicable-hint warnings even when the chosen plan doesn't match the explicit INL_* constraint.

Root cause locations:

  • pkg/planner/core/operator/logicalop/logical_join.go: Sets PreferIndexJoinFirst unconditionally even when INL_* hints are present; containDifferentJoinTypes() masks it out so no conflict warning is raised
  • pkg/planner/core/exhaust_physical_plans.go:2272: preferIndexJoinFamily() early-returns on PreferIndexJoinFirst before checking Prefer{Left,Right}AsINL{J,HJ,MJ}Inner constraints

Suggested fix: Check INL_* constraints first in preferIndexJoinFamily(), or prevent setting PreferIndexJoinFirst when an INL_* inner-side is already forced. Consider adding a regression test for INDEX_JOIN_FIRST() + INL_HASH_JOIN()/INL_JOIN() combinations.

@tiprow
Copy link

tiprow bot commented Feb 28, 2026

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

Test name Commit Details Required Rerun command
fast_test_tiprow 400a7b7 link true /test fast_test_tiprow

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.

@codecov
Copy link

codecov bot commented Feb 28, 2026

Codecov Report

❌ Patch coverage is 95.23810% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.9880%. Comparing base (8833679) to head (400a7b7).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #66596        +/-   ##
================================================
+ Coverage   77.6870%   77.9880%   +0.3010%     
================================================
  Files          2007       2008         +1     
  Lines        549532     549596        +64     
================================================
+ Hits         426915     428619      +1704     
+ Misses       120957     119317      -1640     
  Partials       1660       1660                
Flag Coverage Δ
unit 76.6645% <95.2381%> (+0.3250%) ⬆️

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

Components Coverage Δ
dumpling 56.7974% <ø> (ø)
parser ∅ <ø> (∅)
br 60.9091% <ø> (+0.0236%) ⬆️
🚀 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.

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

Labels

do-not-merge/needs-tests-checked ok-to-test Indicates a PR is ready to be tested. release-note Denotes a PR that will be considered when it comes time to generate release notes. 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: support a hint or var like index-join-first to indicate the planner to prefer IndexJoin over other joins

1 participant