Skip to content

planner: Adjust cost for semijoin and apply | tidb-test=pr/2703 - #66786

Merged
ti-chi-bot[bot] merged 17 commits into
pingcap:masterfrom
terry1purcell:semicost
Mar 18, 2026
Merged

planner: Adjust cost for semijoin and apply | tidb-test=pr/2703#66786
ti-chi-bot[bot] merged 17 commits into
pingcap:masterfrom
terry1purcell:semicost

Conversation

@terry1purcell

@terry1purcell terry1purcell commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: ref #65556

Problem Summary:

What changed and how does it work?

Changes

This PR improves planner cost modeling for semi-join operators (EXISTS/NOT EXISTS subqueries) across IndexJoin and Apply plans, particularly when ordering is involved.

  • IndexJoin semi-join must read all inner rows: IndexJoin batches probe keys and reads ALL matching inner rows for the batch, unlike Apply which can short-circuit per outer row. Added a cost penalty proportional to inner rows per key for semi/anti-semi joins to reflect this over-read (plan_cost_ver2.go).
  • Extract calcOuterExpectedCnt helper: Refactored the outer expected row count logic from constructIndexJoinStatic into a shared calcOuterExpectedCnt function, reusable by both IndexJoin and Apply (exhaust_physical_plans.go).
  • Apply ordering index selectivity ratio: Extended tidb_opt_ordering_index_selectivity_ratio to Apply operators, so ordered correlated subqueries model the extra outer rows needed before the inner side produces enough matches — same logic already used by IndexJoin.
  • Fix Apply DeriveStats for semi-joins: LogicalApply.DeriveStats previously always set output row count equal to the outer child, making the ordering ratio a no-op. Now applies SelectionFactor (0.8) for SemiJoin/AntiSemiJoin Apply operators, consistent with LogicalJoin.DeriveStats (logical_apply.go). While 0.8 is a default and a more accurate approach would be to cost more like a join - this is a larger and more disruptive fix. The change here is to recognize that a semi-join may have "some" filtering.
  • Add test for Apply ordering ratio: Added TestOrderingIdxSelectivityRatioForApply using mocked statistics, verifying that increasing the ratio monotonically increases plan cost when an Apply plan is chosen with ORDER BY + LIMIT.

Several testcases were regenerated to reflect the default "SelectionFactor" usage for apply.

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

  • Refactor

    • Improved cost estimation for semi/anti-semi join cases and standardized outer-row count calculations for ordered joins, yielding more consistent query planning.
  • Tests

    • Updated TPCH plan expectations to match revised planner outputs and costs.
    • Added a new test covering ordering-index selectivity effects on correlated subqueries and adjusted test shard count.

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

pantheon-ai Bot commented Mar 8, 2026

Copy link
Copy Markdown

Review Complete

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

ℹ️ Learn more details on Pantheon AI.

@ti-chi-bot ti-chi-bot Bot added size/M Denotes a PR that changes 30-99 lines, ignoring generated files. sig/planner SIG: Planner labels Mar 8, 2026
@tiprow

tiprow Bot commented Mar 8, 2026

Copy link
Copy Markdown

Hi @terry1purcell. Thanks for your PR.

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

I understand the commands that are listed here.

Details

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

@coderabbitai

coderabbitai Bot commented Mar 8, 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

Centralized ordered-join outer-row estimation and added a probe-cost penalty for semi/anti-semi index-join variants; updated TPCH test expectations and added an Apply ordering selectivity test and related stats/DeriveStats adjustments.

Changes

Cohort / File(s) Summary
Index-join cost adjustment
pkg/planner/core/plan_cost_ver2.go
Multiply probeCost by probeRowsOne for Semi/AntiSemi (including LeftOuterSemi/AntiLeftOuterSemi) index-join cases when probeRowsOne > 1, adding a penalty to reflect over-reading inner rows per key.
Ordered-join outer-row estimation refactor
pkg/planner/core/exhaust_physical_plans.go
Added calcOuterExpectedCnt helper and replaced inline outer-expected-count logic in constructIndexJoinStatic and exhaustPhysicalPlans4LogicalApply, centralizing ordering-aware outer row count computation.
Logical Apply stats adjustment
pkg/planner/core/operator/logicalop/logical_apply.go
In LogicalApply.DeriveStats, use left-child rowCount (scaled by selection factor for Semi/Anti) for derived StatsInfo and adjust ColNDV population for columns from the right side.
Cardinality tests & config
pkg/planner/cardinality/selectivity_test.go, pkg/planner/cardinality/BUILD.bazel
Added TestOrderingIdxSelectivityRatioForApply test (verifies cost sensitivity to tidb_opt_ordering_index_selectivity_ratio for Apply) and incremented test shard_count from 46 to 47.
TPCH planner test fixtures
pkg/planner/core/casetest/tpch/testdata/tpch_suite_out.json, pkg/planner/core/casetest/tpch/testdata/tpch_suite_xut.json
Updated many expected plan texts and cost numbers across TPCH test cases to match new cost/plan construction behavior.

Sequence Diagram(s)

(Skipped — changes are localized planner/cost refactors and test updates that do not introduce a new multi-component runtime flow requiring a sequence diagram.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • winoros
  • AilinKid

Poem

🐰 I nibble costs beneath the trees,

Where semi-joins and orders freeze,
A little helper counts the rows,
So probing knows where extra goes,
Hoppity plans, optimized with ease!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% 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 reflects the main changes: cost adjustments for semijoin and apply operators in the planner.
Description check ✅ Passed The PR description is comprehensive, covering problem statement, technical changes, implementation details, and testing. However, the Issue Number field is incomplete (ref #65556 rather than close/ref with proper link).

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

You can enable review details to help with troubleshooting, context usage and more.

Enable the reviews.review_details setting to include review details such as the model used, the time taken for each step and more in the review comments.

@hawkingrei

Copy link
Copy Markdown
Contributor

/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 8, 2026

@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 (1)
pkg/planner/core/plan_cost_ver2.go (1)

808-810: Prefer p.JoinType.IsSemiJoin() here.

pkg/planner/core/base/plan_base.go already centralizes the four semi/anti-semi variants. Reusing it avoids duplicating join-type logic in the cost model.

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

In `@pkg/planner/core/plan_cost_ver2.go` around lines 808 - 810, Replace the
explicit join-type checks for semi/anti-semi joins in the IndexHashJoin branch
by using the centralized helper: call p.JoinType.IsSemiJoin() instead of
comparing against base.SemiJoin, base.AntiSemiJoin, base.LeftOuterSemiJoin, and
base.AntiLeftOuterSemiJoin; update the condition in the IndexHashJoin check
(around the indexJoinType == 1 && probeRowsOne > 1 block in plan_cost_ver2.go)
to use p.JoinType.IsSemiJoin() so it leverages the logic defined in plan_base.go
and removes duplicated join-type logic.
🤖 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/plan_cost_ver2.go`:
- Around line 803-813: The semi/anti-semi branch is applying probeRowsOne to an
already batch-discounted probeCost (variable probeCost), leaving semi-joins
under-costed; fix by computing the semi-join path from the non-discounted base:
for indexJoinType == 1 and p.JoinType in {base.SemiJoin, base.AntiSemiJoin,
base.LeftOuterSemiJoin, base.AntiLeftOuterSemiJoin} restore the original
(non-batched) probe cost by multiplying the current probeCost by batchRatio (the
variable used earlier) to undo the division, then apply costusage.MulCostVer2
with probeRowsOne; i.e., set probeCost =
costusage.MulCostVer2(costusage.MulCostVer2(probeCost, batchRatio),
probeRowsOne) so the penalty uses the undiscouned base.

---

Nitpick comments:
In `@pkg/planner/core/plan_cost_ver2.go`:
- Around line 808-810: Replace the explicit join-type checks for semi/anti-semi
joins in the IndexHashJoin branch by using the centralized helper: call
p.JoinType.IsSemiJoin() instead of comparing against base.SemiJoin,
base.AntiSemiJoin, base.LeftOuterSemiJoin, and base.AntiLeftOuterSemiJoin;
update the condition in the IndexHashJoin check (around the indexJoinType == 1
&& probeRowsOne > 1 block in plan_cost_ver2.go) to use p.JoinType.IsSemiJoin()
so it leverages the logic defined in plan_base.go and removes duplicated
join-type logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a1d5d951-0f15-4523-b456-968e4c0e7352

📥 Commits

Reviewing files that changed from the base of the PR and between 571b516 and 15b9121.

📒 Files selected for processing (3)
  • pkg/planner/core/casetest/tpch/testdata/tpch_suite_out.json
  • pkg/planner/core/casetest/tpch/testdata/tpch_suite_xut.json
  • pkg/planner/core/plan_cost_ver2.go

Comment thread pkg/planner/core/plan_cost_ver2.go Outdated
Comment thread pkg/planner/core/plan_cost_ver2.go Outdated
@codecov

codecov Bot commented Mar 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.4895%. Comparing base (f33d777) to head (1ff678a).
⚠️ Report is 21 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #66786        +/-   ##
================================================
- Coverage   77.7308%   77.4895%   -0.2414%     
================================================
  Files          2012       1937        -75     
  Lines        551135     545803      -5332     
================================================
- Hits         428402     422940      -5462     
- Misses       121017     122832      +1815     
+ Partials       1716         31      -1685     
Flag Coverage Δ
integration 41.1522% <100.0000%> (-7.0039%) ⬇️
unit 76.6092% <100.0000%> (+0.4114%) ⬆️

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

Components Coverage Δ
dumpling 57.2380% <ø> (+0.4405%) ⬆️
parser ∅ <ø> (∅)
br 48.0680% <ø> (-12.8341%) ⬇️
🚀 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.

@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

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

Inline comments:
In `@tests/integrationtest/r/tpch.result`:
- Line 295: The test expectations were changed while the planner cost change
wasn't applied: the semi/anti-semi over-read penalty is only coded for
indexJoinType == IndexHashJoin in function(s) handling semi/anti joins (refer to
plan_cost_ver2.go and the indexJoinType/IndexHashJoin symbols); update the cost
logic so the over-read penalty also applies to the other IndexJoin variant (or
more generally to any index-based semijoin) by broadening the conditional that
currently checks indexJoinType == IndexHashJoin (or by switching to a check
based on join strategy rather than just the IndexHashJoin constant), then re-run
the planner tests and re-record the golden outputs instead of changing
expectations first.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4d52641d-39c0-4290-8cdd-df5bab930468

📥 Commits

Reviewing files that changed from the base of the PR and between 15b9121 and 1688ee6.

📒 Files selected for processing (1)
  • tests/integrationtest/r/tpch.result

Comment thread tests/integrationtest/r/tpch.result Outdated
terry1purcell and others added 3 commits March 10, 2026 06:59
Deduplicate the OptOrderingIdxSelRatio-aware ExpectedCnt computation
that was inlined in constructIndexJoinStatic into a shared helper.
This avoids duplicating the same logic when other join operators
(e.g. Apply) need the same calculation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ti-chi-bot ti-chi-bot Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Mar 10, 2026
@terry1purcell terry1purcell changed the title planner: IndexJoin semijoin must read all inner rows planner: IndexHashJoin semijoin must read all inner rows Mar 10, 2026

@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

🤖 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/exhaust_physical_plans.go`:
- Around line 258-267: calcOuterExpectedCnt currently adds the rowsToMeetFirst
ordering penalty whenever OptOrderingIdxSelRatio > 0, which incorrectly
penalizes unordered index join candidates; change calcOuterExpectedCnt to only
include the rowsToMeetFirst term when the property indicates an ordered output
(e.g., when prop.SortItems is non-empty) so unordered joins keep their original
ExpectedCnt; update the conditional around rowsToMeetFirst to check
len(prop.SortItems) > 0 (or equivalent) before computing/adding rowsToMeetFirst,
and apply the same guarded change to the analogous logic referenced elsewhere in
this diff (the other calc/return path for static IndexJoin/IndexHashJoin).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 72521dd8-e86a-4b93-9a1c-657d3e3815f0

📥 Commits

Reviewing files that changed from the base of the PR and between 1688ee6 and 8d63e0a.

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

Comment thread pkg/planner/core/exhaust_physical_plans.go
@terry1purcell terry1purcell changed the title planner: IndexHashJoin semijoin must read all inner rows planner: IndexJoin semijoin must read all inner rows Mar 10, 2026
@terry1purcell

Copy link
Copy Markdown
Contributor Author

/retest-required

@terry1purcell terry1purcell changed the title planner: IndexJoin semijoin must read all inner rows planner: Adjust cost for semijoin and apply Mar 10, 2026

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

🧹 Nitpick comments (1)
pkg/planner/cardinality/selectivity_test.go (1)

1917-1924: Minor style observation (optional).

The hasApply helper is appropriately scoped. Consider extracting the cost-parsing pattern (lines 1930-1931, 1936-1937, etc.) into a shared helper if more similar tests are added in the future, though this matches the existing TestOrderingIdxSelectivityRatioForJoin style.

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

In `@pkg/planner/cardinality/selectivity_test.go` around lines 1917 - 1924, The
cost-parsing logic repeated in the test (the pattern that parses cost values
from rows) should be extracted into a small helper (e.g., parsePlanCostFromRow
or extractCostFromRow) and used by this test and any similar ones like
TestOrderingIdxSelectivityRatioForJoin; implement a helper that accepts a row
([]interface{}) or the plan string, returns the parsed float64 cost (with error
handling), replace the duplicated parsing snippets in the test and in other
tests with calls to that helper, and keep the existing hasApply closure as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/planner/cardinality/selectivity_test.go`:
- Around line 1917-1924: The cost-parsing logic repeated in the test (the
pattern that parses cost values from rows) should be extracted into a small
helper (e.g., parsePlanCostFromRow or extractCostFromRow) and used by this test
and any similar ones like TestOrderingIdxSelectivityRatioForJoin; implement a
helper that accepts a row ([]interface{}) or the plan string, returns the parsed
float64 cost (with error handling), replace the duplicated parsing snippets in
the test and in other tests with calls to that helper, and keep the existing
hasApply closure as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e55c080c-230f-47b2-8985-3390dcc9328b

📥 Commits

Reviewing files that changed from the base of the PR and between 593267f and 40e724d.

📒 Files selected for processing (3)
  • pkg/planner/cardinality/BUILD.bazel
  • pkg/planner/cardinality/selectivity_test.go
  • pkg/planner/core/operator/logicalop/logical_apply.go

@terry1purcell terry1purcell changed the title planner: Adjust cost for semijoin and apply planner: Adjust cost for semijoin and apply | tidb-test/pr=2703 Mar 10, 2026
@ti-chi-bot ti-chi-bot Bot added the needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label Mar 12, 2026

@AilinKid AilinKid 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.

rest LGTM

Comment thread pkg/planner/core/operator/logicalop/logical_apply.go
Comment thread pkg/planner/core/plan_cost_ver2.go
@ti-chi-bot

ti-chi-bot Bot commented Mar 14, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

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

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 lgtm and removed needs-1-more-lgtm Indicates a PR needs 1 more LGTM. labels Mar 14, 2026
@ti-chi-bot

ti-chi-bot Bot commented Mar 14, 2026

Copy link
Copy Markdown

[LGTM Timeline notifier]

Timeline:

  • 2026-03-12 09:09:09.077432762 +0000 UTC m=+513980.589490423: ☑️ agreed by qw4990.
  • 2026-03-14 02:31:44.880348 +0000 UTC m=+60161.881121486: ☑️ agreed by AilinKid.

@ti-chi-bot ti-chi-bot Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Mar 14, 2026
@terry1purcell

Copy link
Copy Markdown
Contributor Author

/hold

Other review comments - that aren't critical to merge. But are worthy of resolution.

@ti-chi-bot ti-chi-bot Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Mar 14, 2026
@ti-chi-bot ti-chi-bot Bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Mar 17, 2026
@terry1purcell

Copy link
Copy Markdown
Contributor Author

/unhold

@ti-chi-bot ti-chi-bot Bot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Mar 17, 2026
@terry1purcell

Copy link
Copy Markdown
Contributor Author

/retest-required

@ti-chi-bot
ti-chi-bot Bot merged commit 102f7af into pingcap:master Mar 18, 2026
35 checks passed
@terry1purcell
terry1purcell deleted the semicost branch March 18, 2026 01:43
AilinKid pushed a commit to AilinKid/tidb that referenced this pull request May 14, 2026
AilinKid pushed a commit to AilinKid/tidb that referenced this pull request May 14, 2026
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/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants