Skip to content

planner: credit appended handle predicates in index row count estimation#69749

Open
terry1purcell wants to merge 3 commits into
pingcap:masterfrom
terry1purcell:handlebackoff
Open

planner: credit appended handle predicates in index row count estimation#69749
terry1purcell wants to merge 3 commits into
pingcap:masterfrom
terry1purcell:handlebackoff

Conversation

@terry1purcell

@terry1purcell terry1purcell commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #69748

Problem Summary:

For a non-unique secondary index, predicates on the handle columns become part of the execution ranges (the physical key ends with the handle), but the path's CountAfterAccess is estimated from ranges pruned back to the declared index columns and gives those predicates no credit. a = 5 AND id = 7 on KEY ia(a) builds the point range [5 7,5 7], which matches at most one row because the physical key (a, id) is unique — yet it estimates the full a = 5 prefix count. A path whose benefit is the handle seek looks as expensive as one without it and can lose path selection.

Worse, Selectivity() does credit these predicates (through exponential backoff over Idx2ColUniqueIDs, which includes the appended handle), so any attempt to credit CountAfterAccess that lands below stats.RowCount was treated by adjustCountAfterAccess as inconsistent assumptions and penalized with 1/SelectionFactor — making the credited path more expensive (12.50 instead of 10.00 in the test scenario).

Depends on #69747 (this branch stacks on it; will rebase once it merges).

What changed and how does it work?

New cardinality.AdjustRowCountForAppendedHandleColumns, applied in detachCondAndBuildRangeForPath after the pruned-prefix estimate:

  • The prefix estimate (from index statistics over the declared columns) keeps the full-weight exponential-backoff slot; each appended handle column contributes sel^(1/2), sel^(1/4), ... starting from the most selective, using its column statistics. This credits the handle predicates without assuming independence between the index columns and the primary key.
  • Per-column ranges extracted from the handle dimensions are merged with ranger.UnionRanges first, so ranges differing only in earlier dimensions do not double count.
  • When every range point-binds the declared columns plus the full handle, the estimate is capped at the number of ranges (the physical key is unique).
  • A column not bound in every range, or without valid statistics, contributes nothing.

Consistency with Selectivity(): when the damped CountAfterAccess falls below stats.RowCount, adjustCountAfterAccess now aligns it upward to stats.RowCount without the SelectionFactor penalty for handle-extended ranges — the shortfall is expected damping, not the inconsistent-assumption case the penalty exists for.

Resulting estimates on the test table (100 rows, 10 per value of a, previously all 10.00):

Query estRows
a = 5 and id = 7 1.00
a = 5 and id in (11, 22) 2.00
a = 5 and id > 10 10.00 (unchanged)

Non-point handle predicates receive no net credit yet: expBackoffEstimation floors its result at 1/NDV of the declared index columns, so the alignment brings them back to the prefix estimate. Lifting that floor requires truncating the full-key bounds used by the multi-column histogram upper-limit computation and is left to a follow-up (see the issue).

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.

Improve the row count estimation of non-unique secondary index scans whose ranges include predicates on the primary key, so that access paths benefiting from primary key seeks are costed more accurately.

Summary by CodeRabbit

  • Bug Fixes

    • Improved row-count and selectivity estimates for non-unique index scans when execution ranges extend into appended handle columns.
    • Reduced over-penalization for partially constrained handle/index ranges and improved point-lookup accuracy.
    • Added safer capping for full-point key ranges to prevent overstated cardinality in plans.
  • Tests

    • Added coverage for truncated and appended handle range scenarios to validate updated index range estimation behavior.

terry1purcell and others added 2 commits July 9, 2026 16:29
pruneEstimateRange truncates ranges built over the index columns plus
the appended handle columns down to the declared index columns so they
align with index statistics. The truncation kept the original exclusion
flags and estimated every pruned range independently, which breaks in
two ways once a handle predicate extends the range:

- An exclusive bound whose values were dropped collapses the range to
  empty: a = 5 AND id > 10 builds (5 10, 5 +inf], which pruned to
  (5, 5] and estimated ~0 rows regardless of the selectivity of a = 5.
- Multiple handle points under one prefix double count: a = 5 AND
  id IN (11, 22) pruned to [5,5], [5,5] and counted the a = 5 rows
  twice.

Make a truncated bound inclusive, since dropping trailing dimensions
widens it to the whole prefix, and merge the pruned ranges with
ranger.UnionRanges so overlapping prefixes are counted once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
For a non-unique secondary index, predicates on the handle columns become
part of the execution ranges, but the path's CountAfterAccess is estimated
from ranges pruned back to the declared index columns and gave those
predicates no credit: a path whose benefit is the handle seek looked as
expensive as one without it.

Damp the pruned prefix estimate with the handle columns' selectivities
using the exponential backoff series - the prefix keeps the full-weight
slot and each handle column contributes sel^(1/2), sel^(1/4), ... from the
most selective one - so the handle predicates tighten the estimate without
assuming independence between the index columns and the primary key. A
range that point-binds the declared columns plus the full handle matches
at most one row because the physical key is unique, so such estimates are
additionally capped by the number of ranges.

Selectivity() already credits these predicates through exponential
backoff, so when the damped CountAfterAccess falls below stats.RowCount,
align it upward to stats.RowCount without the SelectionFactor penalty:
the shortfall is expected damping, not the inconsistent-assumption case
the penalty exists for. Non-point handle predicates currently receive no
net credit after this alignment because expBackoffEstimation floors its
result at 1/NDV of the declared index columns; lifting that floor needs
the multi-column histogram bounds to be truncated first and is left as a
follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ti-chi-bot ti-chi-bot Bot added the release-note Denotes a PR that will be considered when it comes time to generate release notes. label Jul 10, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

[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 ailinkid 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

@ti-chi-bot ti-chi-bot Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. sig/planner SIG: Planner labels Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c7f5b8f9-c185-4f5c-8cca-1de287a2d326

📥 Commits

Reviewing files that changed from the base of the PR and between fd7b3f8 and 4388972.

📒 Files selected for processing (1)
  • pkg/planner/cardinality/row_count_index.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/planner/cardinality/row_count_index.go

📝 Walkthrough

Walkthrough

Changes

The planner preserves declared-column prefix estimates while crediting appended handle predicates through damped selectivity, point-range caps, corrected range pruning, and adjusted access-count consistency handling. Tests cover exclusive and point handle predicates.

Appended Handle Cardinality

Layer / File(s) Summary
Handle selectivity adjustment
pkg/planner/cardinality/row_count_index.go
Adds damped appended-handle selectivity, lower bounds, and full-point range caps.
Planner range and count integration
pkg/planner/core/stats.go, pkg/planner/core/BUILD.bazel
Prunes and merges declared-column ranges, applies handle adjustments, and updates access-count handling and build dependencies.
Range estimation validation
pkg/planner/cardinality/selectivity_test.go
Tests execution ranges and estimated rows for exclusive-bound and point-bound handle predicates.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Query as Query predicates
  participant Planner as Planner range estimation
  participant Stats as Column and index statistics
  participant Path as Index access path
  Query->>Planner: Build index ranges
  Planner->>Stats: Estimate declared-column prefix
  Stats-->>Planner: Prefix row estimate
  Planner->>Stats: Estimate appended-handle selectivity
  Stats-->>Planner: Damped handle adjustment
  Planner->>Path: Store adjusted access counts
Loading

Possibly related issues

  • 69746 — The updated range pruning and merging address truncated handle-range behavior described by this issue.
  • 69744 — The changes adjust estimation after appended handles are present, complementing the issue’s range-construction scope.

Suggested reviewers: windtalker, qw4990, AilinKid

Poem

I’m a rabbit with ranges to prune,
Damping estimates beneath the moon.
Handles append, and counts align,
Point keys cap at one—so fine!
Tests hop through every scan,
Cardinality follows the plan.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main planner change: crediting appended handle predicates in index row-count estimation.
Linked Issues check ✅ Passed The changes implement the requested exponential-backoff credit, range merging, estimate capping, and CountAfterAccess alignment for #69748.
Out of Scope Changes check ✅ Passed The modified files stay focused on planner row-count estimation and its supporting test/build updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


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.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.06306% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.2951%. Comparing base (52f7a7a) to head (4388972).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #69749        +/-   ##
================================================
- Coverage   76.3195%   74.2951%   -2.0244%     
================================================
  Files          2041       2054        +13     
  Lines        560153     579424     +19271     
================================================
+ Hits         427506     430484      +2978     
- Misses       131746     148322     +16576     
+ Partials        901        618       -283     
Flag Coverage Δ
integration 40.9220% <63.0630%> (+1.2167%) ⬆️

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

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

Copilot AI 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.

Pull request overview

This PR improves TiDB planner cardinality estimation for non-unique secondary index access paths whose execution ranges include appended handle (primary key) columns, so that handle predicates influence CountAfterAccess and path costing more accurately (and avoids the SelectionFactor penalty when the damped estimate falls below stats.RowCount).

Changes:

  • Add appended-handle-aware row-count damping (AdjustRowCountForAppendedHandleColumns) using exponential backoff over handle-column selectivities, with a cap for full-point (unique physical key) ranges.
  • Update index-range estimation flow to prune/union estimation ranges with a RangerContext, and adjust adjustCountAfterAccess behavior when ranges include appended handle columns.
  • Add unit tests covering handle-extended ranges (exclusive bounds, IN-list point ranges, and full point lookups), and update Bazel deps.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
pkg/planner/core/stats.go Detects handle-extended ranges, prunes/merges estimation ranges with RangerContext, applies appended-handle damping, and avoids SelectionFactor penalty when aligning to stats.RowCount for handle-extended ranges.
pkg/planner/core/BUILD.bazel Adds //pkg/util/ranger/context dependency to support the new pruning/union API usage.
pkg/planner/cardinality/selectivity_test.go Adds a regression/unit test validating estimation behavior for truncated/handle-extended index ranges and point-range capping.
pkg/planner/cardinality/row_count_index.go Introduces AdjustRowCountForAppendedHandleColumns to credit appended handle predicates via exponential backoff and cap full-point ranges.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/planner/cardinality/row_count_index.go Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

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

Labels

release-note Denotes a PR that will be considered when it comes time to generate release notes. 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.

planner: credit appended handle column predicates in index row count estimation using exponential backoff

2 participants