Skip to content

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

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

planner: credit appended handle predicates in index row count estimation#69749
terry1purcell wants to merge 8 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/selectivity estimation for non-unique index scans when execution ranges include appended handle columns, aligning results for partially constrained predicates.
    • Added handling for pruned estimation ranges and refined damping/capping of estimates for point-bound handle predicates to avoid over/under-penalization.
    • Ensured unsigned handle predicates don’t expand the index execution range.
  • Tests
    • Added coverage for truncated/appended handle range behavior across multiple predicate shapes, including unsigned cases, validating estimated rows and execution-range preservation.

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

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

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: ac4bbc9e-7694-418b-9eeb-a3afaeaf9f6e

📥 Commits

Reviewing files that changed from the base of the PR and between 18f3c60 and 6b61609.

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

📝 Walkthrough

Walkthrough

The planner now credits appended handle predicates with damped selectivity, preserves corrected truncated ranges, aligns access counts, and caps full-point estimates. Tests cover exclusive, point, and unsigned clustered-handle predicates.

Changes

Appended Handle Cardinality

Layer / File(s) Summary
Handle selectivity adjustment
pkg/planner/cardinality/row_count_index.go
Adds damped appended-handle selectivity, lower-bound handling, 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, aligns access counts, and adds the ranger context dependency.
Range estimation validation
pkg/planner/cardinality/selectivity_test.go
Tests execution ranges and estimates for exclusive, point, and unsigned-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 PRs

  • pingcap/tidb#69745 — Adds appended clustered/common-handle columns to related index access paths.

Suggested reviewers: winoros, 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
Title check ✅ Passed The title clearly summarizes the main change: crediting appended handle predicates in index row-count estimation.
Description check ✅ Passed The description follows the template well, includes the issue number, problem, changes, tests, and release note.
Linked Issues check ✅ Passed The code changes match #69748 by crediting appended handle predicates, merging ranges, capping point-bound estimates, and keeping CountAfterAccess consistent.
Out of Scope Changes check ✅ Passed The added Bazel dependency and new test support the planner change and do not appear unrelated to the stated objective.
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

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 62.37624% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.9943%. Comparing base (d573e28) to head (3a056c0).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #69749        +/-   ##
================================================
- Coverage   76.3206%   73.9943%   -2.3264%     
================================================
  Files          2041       2057        +16     
  Lines        559973     578424     +18451     
================================================
+ Hits         427375     428001       +626     
- Misses       131697     150087     +18390     
+ Partials        901        336       -565     
Flag Coverage Δ
integration 40.7690% <62.3762%> (+1.0638%) ⬆️

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.

sctx planctx.PlanContext,
coll *statistics.HistColl,
ranges []*ranger.Range,
idxCols []*expression.Column,

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.

How about renaming this argument to "appendedIdxCols" to represent that it contains both the declared index columns and PK columns?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed the name should distinguish this from the idxCols params elsewhere in this file, which receive only the declared columns. My only hesitation with appendedIdxCols is it could be read as containing only the appended handle columns. How about idxColsWithHandle? (I would avoid fullIdxCols since AccessPath.FullIdxCols already means the declared columns without the handle.) Renamed to idxColsWithHandle — happy to adjust if you prefer another name.

// handle and is therefore unique.
fullPoints := true
for _, ran := range ranges {
if len(ran.LowVal) != len(idxCols) || len(ran.HighVal) != len(idxCols) ||

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.

It seems like there is an implicit assumption: the idxCols contains all PK columns? Could we add a comment for this implicit assumption?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — yes, the cap is only valid because the appended dimensions cover the complete handle. That is guaranteed by fillIndexPath, which appends the handle only when the primary key is the single integer handle column (GetPKIsHandleCol), so the appended suffix always identifies a row exactly. Added a comment documenting this contract on the function doc and at the full-point cap.

@ti-chi-bot ti-chi-bot Bot added approved needs-1-more-lgtm Indicates a PR needs 1 more LGTM. labels Jul 13, 2026
Rename the idxCols parameter of AdjustRowCountForAppendedHandleColumns to
idxColsWithHandle to distinguish it from the declared-columns-only idxCols
parameters elsewhere in this file, and document the contract that the
appended dimensions cover the complete integer handle, which the
full-point one-row cap relies on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@winoros winoros left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's a special fact in TiDB:
TiKV stores the unsigned int pk in a wrong physical order.
It stores the part [MaxInt64+1, MaxUint64] before [0, MaxInt64]. But statistics doesn't handle this. I think we need to check the behavior and test it.

@ti-chi-bot ti-chi-bot Bot removed the needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label Jul 14, 2026
… estimation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@terry1purcell

Copy link
Copy Markdown
Contributor Author

There's a special fact in TiDB: TiKV stores the unsigned int pk in a wrong physical order. It stores the part [MaxInt64+1, MaxUint64] before [0, MaxInt64]. But statistics doesn't handle this. I think we need to check the behavior and test it.

Thank you. Upon investigation - this is exactly why fillIndexPath has never appended an unsigned handle (!HasUnsignedFlag guard, predates this PR), so the new credit/prune code can't fire for unsigned PKs. The latest commit added an unsigned-PK case to the test to pin that down.

@terry1purcell terry1purcell requested a review from winoros July 14, 2026 21:16
@ti-chi-bot

ti-chi-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

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

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 needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label Jul 16, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

[LGTM Timeline notifier]

Timeline:

  • 2026-07-13 10:44:24.629440772 +0000 UTC m=+624050.665535838: ☑️ agreed by qw4990.
  • 2026-07-14 14:49:01.34906821 +0000 UTC m=+725127.385163265: ✖️🔁 reset by winoros.
  • 2026-07-16 13:40:46.368760617 +0000 UTC m=+893832.404855673: ☑️ agreed by winoros.

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

Labels

approved needs-1-more-lgtm Indicates a PR needs 1 more LGTM. 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

4 participants