Skip to content

planner, executor: plan ordered window for ordered input - #68520

Open
hawkingrei wants to merge 20 commits into
pingcap:masterfrom
hawkingrei:planner-ordered-window
Open

hawkingrei wants to merge 20 commits into
pingcap:masterfrom
hawkingrei:planner-ordered-window

Conversation

@hawkingrei

@hawkingrei hawkingrei commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: ref #67989

Problem Summary:

PR #68480 added the executor-side ordered window builder but left planner enumeration out of scope. This PR lets the optimizer choose an ordered-input window when the child naturally provides the required partition/order property, including index-join paths without join hints.

What changed and how does it work?

  • Add PhysicalOrderedWindow for supported row_number() windows whose child already satisfies the window property. If a Sort enforcer is required, the plan stays on the regular Window path.
  • Wire the planner and executor bridge needed by the new physical plan.
  • Propagate index-join runtime keys through simple projections so index join can still match the underlying datasource columns.
  • Support backward scans for partition by ... order by ... desc on indexes such as (partition_key, order_time).
  • Add casetest coverage for ordered index input, fallback paths, index join without hints, and a desensitized plan-replayer shape.

Example:

The main regression query joins a filtered probe table to a derived inner table and keeps only the latest row per partition key. The query itself has no join hint:

select p.probe_key,
       case when f.type_code = 4 and f.state_code = 2 then 1 else 0 end as matched,
       f.payload
from probe_rows p
left join (
  select *
  from (
    select i.partition_key, i.type_code, i.state_code, i.payload, i.order_time,
           row_number() over(partition by i.partition_key order by i.order_time desc) as rn
    from inner_rows i
  ) ranked
  where rn = 1
) f on p.probe_key = f.partition_key
where p.probe_time > '2026-02-04 00:00:00'
  and p.probe_time <= '2026-02-05 00:00:00'
  and p.filter_code = 'filter_001'
  and ifnull(p.flag1, 0) = 0
  and ifnull(p.flag2, 0) = 0
  and ifnull(p.flag3, 0) = 0;

Before this PR, the window side had to be fully materialized and sorted:

MergeJoin root  left outer join, left side:Sort, left key:test.probe_rows.probe_key, right key:test.inner_rows.partition_key
├─Selection(Build) root  eq(Column, 1)
│ └─Window root  row_number()->Column over(partition by test.inner_rows.partition_key order by test.inner_rows.order_time desc rows between current row and current row)
│   └─Sort root  test.inner_rows.partition_key, test.inner_rows.order_time:desc
│     └─TableReader root  data:TableFullScan
│       └─TableFullScan cop[tikv] table:i keep order:false, stats:pseudo
└─Sort(Probe) root  test.probe_rows.probe_key
  └─IndexLookUp root

After this PR, the optimizer can use IndexHashJoin and build the window side from the ordered (partition_key, order_time) index. The backward index range scan provides the required order, so the Sort and full inner-table scan disappear:

IndexHashJoin root  left outer join, inner:Selection, left side:IndexLookUp, outer key:test.probe_rows.probe_key, inner key:test.inner_rows.partition_key
├─IndexLookUp(Build) root
│ └─IndexRangeScan cop[tikv] table:p, index:idx_filter_time(filter_code, probe_time, flag1, flag2, flag3)
└─Selection(Probe) root  eq(Column, 1)
  └─OrderedWindow root  row_number()->Column over(partition by test.inner_rows.partition_key order by test.inner_rows.order_time desc rows between current row and current row)
    └─Projection root
      └─IndexLookUp root
        ├─IndexRangeScan(Build) cop[tikv] table:i, index:idx_partition_order(partition_key, order_time) range: decided by [eq(test.inner_rows.partition_key, test.probe_rows.probe_key)], keep order:true, desc, stats:pseudo
        └─TableRowIDScan(Probe) cop[tikv] table:i keep order:false, stats:pseudo

The partition TopN pruning part from PR #67696 is not included here and should stay in a follow-up PR.

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.

Commands:

  • ./tools/check/failpoint-go-test.sh pkg/planner/core/casetest/windows -run TestWindowPushDownPlans -count=1
  • ./tools/check/failpoint-go-test.sh pkg/planner/core/casetest/join -run TestIndexJoinWithOrderedWindowInner -record
  • ./tools/check/failpoint-go-test.sh pkg/planner/core/casetest/join -run TestIndexJoinWithOrderedWindowInner
  • ./tools/check/failpoint-go-test.sh pkg/planner/core/casetest/join -count=1
  • ./tools/check/failpoint-go-test.sh pkg/planner/core/casetest/windows -run 'TestWindow(FunctionDescCanPushDown|PlanWithOtherOperators)$' -count=1
  • ./tools/check/failpoint-go-test.sh pkg/planner/core/casetest/windows -run 'TestWindow(PushDownPlans|WithOuterJoinAndCTE)$' -count=1
  • ./tools/check/failpoint-go-test.sh pkg/planner/core/casetest/rule -run TestOuter2InnerSuite -count=1
  • ./tools/check/failpoint-go-test.sh pkg/planner/core/operator/physicalop -run '^$' -count=0
  • (cd tests/integrationtest && ./run-tests.sh -r planner/core/indexjoin)
  • (cd tests/integrationtest && ./run-tests.sh -r planner/core/casetest/rule/rule_derive_topn_from_window)
  • make lint

Attempted but blocked locally:

  • make bazel_prepare was retried, but the latest run failed while fetching cloud.google.com/go/storage@v1.39.1 from goproxy.cn with unexpected EOF. The generated DEPS.bzl noise from the failed run was restored; the relevant join BUILD.bazel testdata metadata is included.

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

@ti-chi-bot ti-chi-bot Bot added release-note-none Denotes a PR that doesn't merit a release note. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. sig/planner SIG: Planner labels May 20, 2026
@coderabbitai

coderabbitai Bot commented May 20, 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 PhysicalOrderedWindow operator and wires it through planner generation, executor builder, task attachment, shuffle optimization, plan stringification, and test fixtures/expectations.

Changes

Ordered Window Operator Optimization

Layer / File(s) Summary
Physical operator definition and interface contract
pkg/planner/core/operator/physicalop/physical_window.go, pkg/planner/core/operator/physicalop/base_physical_plan.go
PhysicalOrderedWindow struct embeds PhysicalWindow. Implements Init, Clone, Attach2Task; adds compile-time base.PhysicalPlan assertion.
Plan generation logic with ordered window eligibility
pkg/planner/core/operator/physicalop/physical_window.go
ExhaustPhysicalPlans4LogicalWindow computes ordered-child properties and emits PhysicalOrderedWindow when CanUseOrderedWindow (restricts to single row_number with ROWS CURRENT ROW frame); falls back to PhysicalWindow.
Executor builder routing for ordered windows
pkg/executor/builder.go
Adds buildOrderedWindow and routes *physicalop.PhysicalOrderedWindow to windowexec.BuildOrdered, propagating errors.
Planner infrastructure integration
pkg/planner/core/optimizer.go, pkg/planner/core/plan.go, pkg/planner/core/task.go, pkg/planner/core/stringer.go
Handle PhysicalOrderedWindow in fine-grained shuffle setup, optimizeByShuffle (and new ordered-window shuffle path), attach2Task4PhysicalWindow (ordered/unordered guards and CopTask validation), and plan stringification.
Test framework and window pushdown cases
pkg/planner/core/casetest/windows/window_push_down_test.go, pkg/planner/core/casetest/windows/testdata/*, .agents/skills/tidb-test-guidelines/references/planner-case-map.md, tests/integrationtest/.../rule_derive_topn_from_window.result
Normalize test SQL for execution-vs-plan branching; add ordered_window_t testcases exercising row_number/rank with partition/order variants and update expected outputs to use OrderedWindow where applicable.

Sequence Diagram(s)

sequenceDiagram
  participant LogicalPlanner
  participant ExhaustPhysicalPlans4LogicalWindow
  participant PhysicalOrderedWindow
  participant ExecutorBuilder
  participant windowexec_BuildOrdered
  LogicalPlanner->>ExhaustPhysicalPlans4LogicalWindow: request physical plans for LogicalWindow
  ExhaustPhysicalPlans4LogicalWindow->>PhysicalOrderedWindow: emit OrderedWindow when CanUseOrderedWindow
  PhysicalOrderedWindow->>ExecutorBuilder: builder.build routes to buildOrderedWindow
  ExecutorBuilder->>windowexec_BuildOrdered: build ordered-window executor
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pingcap/tidb#68480: Executor-side BuildOrdered/OrderedWindowExec support that this planner wiring calls into.
  • pingcap/tidb#67993: Related executor builder refactor around window executor construction.

Suggested labels

size/XXL, ok-to-test

Suggested reviewers

  • guo-shaoge
  • AilinKid
  • qw4990

Poem

🐰 I hopped through plans with careful cheer,
Ordered rows aligned — no Sort is near.
A window snug where order's known,
Executors build, the plan is shown.
Hop, review, and merge — a carrot cheer! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% 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 and concisely summarizes the main change: adding planner support for ordered window operators on already-ordered input.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively follows the template, includes issue reference, detailed problem summary, changes explanation, test coverage, and documentation markers.

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

✨ 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 and usage tips.

@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 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/casetest/windows/window_push_down_test.go`:
- Around line 41-42: The test currently treats queries starting only with
"select" or "explain" as query-path verifiable and sends everything else
(including CTEs starting with "with") to tk.MustExec, which skips plan/warning
checks; update the condition that inspects stmt so that it also recognizes
"with" (case-insensitive, trim leading whitespace) as a query that should go
through the normal plan/warning verification path instead of calling
tk.MustExec(tt). Locate the stmt variable and the branch that calls
tk.MustExec(tt) in window_push_down_test.go (affecting tests like
TestWindowWithOuterJoinAndCTE) and modify the prefix check to include "with" (or
detect leading CTEs and then the underlying "select"/"explain") so CTE queries
are classified correctly.
🪄 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: 43fa2aff-fb42-4eb6-93d0-ce7f07d7a3d6

📥 Commits

Reviewing files that changed from the base of the PR and between 39dbdcc and 7f6d9dc.

📒 Files selected for processing (12)
  • .agents/skills/tidb-test-guidelines/references/planner-case-map.md
  • pkg/executor/builder.go
  • pkg/planner/core/casetest/windows/testdata/window_push_down_suite_in.json
  • pkg/planner/core/casetest/windows/testdata/window_push_down_suite_out.json
  • pkg/planner/core/casetest/windows/testdata/window_push_down_suite_xut.json
  • pkg/planner/core/casetest/windows/window_push_down_test.go
  • pkg/planner/core/operator/physicalop/base_physical_plan.go
  • pkg/planner/core/operator/physicalop/physical_window.go
  • pkg/planner/core/optimizer.go
  • pkg/planner/core/plan.go
  • pkg/planner/core/stringer.go
  • pkg/planner/core/task.go

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/planner/core/casetest/windows/testdata/window_push_down_suite_in.json (1)

22-22: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the result-case deterministic with an explicit outer ORDER BY.

This case asserts exact row sequence but the query has no final ORDER BY, so output order is not guaranteed by SQL semantics and can become flaky with future plan changes.

Suggested change
-      "select a, b, row_number() over(partition by a order by b) as rn from ordered_window_t",
+      "select a, b, row_number() over(partition by a order by b) as rn from ordered_window_t order by a, b",
🤖 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/casetest/windows/testdata/window_push_down_suite_in.json` at
line 22, The test SQL lacks a final ordering so its asserted row sequence can be
flaky; update the query string "select a, b, row_number() over(partition by a
order by b) as rn from ordered_window_t" to include an explicit outer ORDER BY
(for example ORDER BY a, b or ORDER BY rn) so the result order is deterministic;
locate the string in window_push_down_suite_in.json and append the chosen ORDER
BY clause to the query.
🤖 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.

Outside diff comments:
In `@pkg/planner/core/casetest/windows/testdata/window_push_down_suite_in.json`:
- Line 22: The test SQL lacks a final ordering so its asserted row sequence can
be flaky; update the query string "select a, b, row_number() over(partition by a
order by b) as rn from ordered_window_t" to include an explicit outer ORDER BY
(for example ORDER BY a, b or ORDER BY rn) so the result order is deterministic;
locate the string in window_push_down_suite_in.json and append the chosen ORDER
BY clause to the query.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3d240250-6358-4bef-8eb7-ddd543309e57

📥 Commits

Reviewing files that changed from the base of the PR and between 7f6d9dc and 32250fb.

📒 Files selected for processing (3)
  • pkg/planner/core/casetest/windows/testdata/window_push_down_suite_in.json
  • pkg/planner/core/casetest/windows/testdata/window_push_down_suite_out.json
  • pkg/planner/core/casetest/windows/testdata/window_push_down_suite_xut.json

@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/core/operator/physicalop/physical_window.go (1)

69-75: ⚡ Quick win

Use a plancodec constant for the type string.

PhysicalWindow.Init uses plancodec.TypeWindow (line 63), but PhysicalOrderedWindow.Init uses the raw string "OrderedWindow" (line 71). This inconsistency may affect plan hashing and serialization logic that uses the TP() value.

Consider adding TypeOrderedWindow = "OrderedWindow" to the plancodec package and using it here to maintain consistency with other plan types.

🤖 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/operator/physicalop/physical_window.go` around lines 69 -
75, PhysicalOrderedWindow.Init currently calls NewBasePhysicalPlan with the raw
string "OrderedWindow" causing inconsistency with other plan types that use
plancodec constants (e.g., PhysicalWindow.Init uses plancodec.TypeWindow); add a
new constant in the plancodec package (e.g., TypeOrderedWindow =
"OrderedWindow") and replace the literal in PhysicalOrderedWindow.Init so it
calls NewBasePhysicalPlan(ctx, plancodec.TypeOrderedWindow, &p, offset),
preserving consistent TP() values for hashing/serialization.
🤖 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.

Nitpick comments:
In `@pkg/planner/core/operator/physicalop/physical_window.go`:
- Around line 69-75: PhysicalOrderedWindow.Init currently calls
NewBasePhysicalPlan with the raw string "OrderedWindow" causing inconsistency
with other plan types that use plancodec constants (e.g., PhysicalWindow.Init
uses plancodec.TypeWindow); add a new constant in the plancodec package (e.g.,
TypeOrderedWindow = "OrderedWindow") and replace the literal in
PhysicalOrderedWindow.Init so it calls NewBasePhysicalPlan(ctx,
plancodec.TypeOrderedWindow, &p, offset), preserving consistent TP() values for
hashing/serialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9e0bb654-d577-4133-bf4f-94222d7add73

📥 Commits

Reviewing files that changed from the base of the PR and between 32250fb and c2d4331.

📒 Files selected for processing (6)
  • pkg/planner/core/casetest/windows/testdata/window_push_down_suite_in.json
  • pkg/planner/core/casetest/windows/testdata/window_push_down_suite_out.json
  • pkg/planner/core/casetest/windows/testdata/window_push_down_suite_xut.json
  • pkg/planner/core/casetest/windows/window_push_down_test.go
  • pkg/planner/core/operator/physicalop/physical_window.go
  • tests/integrationtest/r/planner/core/casetest/rule/rule_derive_topn_from_window.result

@codecov

codecov Bot commented May 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 53.25203% with 115 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.3814%. Comparing base (39dbdcc) to head (d27342a).
⚠️ Report is 29 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #68520        +/-   ##
================================================
- Coverage   76.6358%   75.3814%   -1.2545%     
================================================
  Files          2027       2021         -6     
  Lines        560954     569416      +8462     
================================================
- Hits         429892     429234       -658     
- Misses       130142     139859      +9717     
+ Partials        920        323       -597     
Flag Coverage Δ
integration 41.4618% <53.2520%> (+1.6798%) ⬆️

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

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

@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 20, 2026
@hawkingrei

Copy link
Copy Markdown
Contributor Author

/retest

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

So many tests changed.
Is this expected?
And I don't see there's a cost formula for the new operator.

@ti-chi-bot ti-chi-bot Bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels May 20, 2026
@qw4990
qw4990 self-requested a review May 21, 2026 02:08
@ti-chi-bot ti-chi-bot Bot removed the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label May 21, 2026
@ti-chi-bot ti-chi-bot Bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label May 21, 2026
@ti-chi-bot

ti-chi-bot Bot commented May 21, 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 hawkingrei 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

@hawkingrei

Copy link
Copy Markdown
Contributor Author

So many tests changed.
Is this expected?
And I don't see there's a cost formula for the new operator.

I've made a lot of updates. Now it's minimal changes, with many newly added tests. The cost part still uses the Windows part. This is currently to let Windows make the most of indexes, and plans that use indexes inherently have lower cost than those that don't.

This branch has not been deployed

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

Labels

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.

2 participants