Repository navigation
Commit 5719930
## Which issue does this PR close?
- Closes #25671.
## Rationale for this change
For `Limit(skip=0, fetch=n)` over a `Sort` without a fetch,
`PushDownLimit` sets `Sort.fetch = n` but keeps the `Limit`. It removes
the `Limit` only on its next visit, in the next optimizer pass. Thus
every `ORDER BY ... LIMIT n` query runs 3 logical optimizer passes where
2 are enough. The final plan does not change. Only planning time does.
A query engine that wraps every query in a row-cap `LIMIT` over an
`ORDER BY` hits this on nearly every query.
Reproduction, `datafusion-cli` on `main` (95bb0a0):
```sql
CREATE TABLE t (a INT, ts TIMESTAMP) AS VALUES (1, TIMESTAMP '2026-01-01T00:00:00');
EXPLAIN VERBOSE SELECT a FROM t ORDER BY ts DESC LIMIT 10;
```
Rules that change the plan, per pass (the `logical_plan after <rule>`
rows that are not `SAME TEXT AS ABOVE`):
```text
main:
pass 0: push_down_limit, optimize_projections
pass 1: push_down_limit <- removes the Limit it kept in pass 0
pass 2: (no change)
this PR:
pass 0: push_down_limit, optimize_projections
pass 1: (no change)
```
The final `logical_plan` is identical on both:
```text
Projection: t.a
Sort: t.ts DESC NULLS FIRST, fetch=10
TableScan: t projection=[a, ts]
```
## What changes are included in this PR?
In `rewrite_limit`, `LogicalPlan::Sort` branch, when the rule gives the
`Sort` a new fetch and `skip == 0`, it now returns the `Sort` and drops
the `Limit` in the same visit. With `skip > 0` the `Limit` stays, as
before. When the `Sort` already has an equal or smaller fetch and `skip
== 0`, `main` already dropped the `Limit`. Both `skip == 0` cases now
share one branch.
One detail needs attention. The optimizer applies `PushDownLimit`
top-down and then recurses into the children of the node that the rule
returns. Before this change, the `Sort` was a child of the returned
`Limit`, so the same pass also visited it and applied the
TopK-through-join pushdown (`push_topk_through_join`). Now the `Sort` is
the returned node, so the traversal does not visit it again in this
pass. The `skip == 0` branch therefore calls `push_topk_through_join` on
the `Sort` directly. Without that call, `Limit -> Sort -> Left Join`
would move the extra pass from the `Limit` removal to the TopK pushdown
instead of removing it. The same was already true on `main` for a `Sort`
that already had a fetch, so that case also gets the TopK pushdown in
the same pass now.
The branch always returns `Transformed::yes`, also when
`push_topk_through_join` returns `Transformed::no`, because the `Limit`
is removed in every case.
## What is the testing strategy for this PR?
- `push_down_limit::test::limit_push_down_sort`: snapshot updated. After
one pass the plan is `Sort: ..., fetch=10` without the `Limit` above it.
- New
`push_down_limit::test::limit_push_down_sort_settles_in_first_pass`:
runs `PushDownLimit` with `max_passes = 3` and asserts that the
optimizer stops after 2 passes (pass 1 leaves the plan unchanged). On
`main` it runs 3.
-
`topk_through_join::test::topk_pushed_through_limit_then_sort_with_two_passes`
is renamed to `topk_pushed_through_limit_then_sort_in_one_pass` and now
uses `max_passes = 1`, with the same expected plan. It fails if the new
branch does not call `push_topk_through_join`.
- New
`topk_through_join::test::topk_pushed_through_limit_over_sort_with_{same,smaller}_fetch_in_one_pass`:
`Limit` over a `Sort` that already has `fetch=3`, over a Left Join, with
`max_passes = 1`. The TopK moves below the join in the one pass. Both
fail on the earlier code.
- New `push_down_limit::test::limit_push_down_sort_reports_transformed`:
calls `PushDownLimit::rewrite` on `Limit -> Sort -> TableScan` (no join)
and asserts `transformed == true`. It fails if the branch returns the
`Transformed` from `push_topk_through_join` as it is.
- `user_defined_plan.rs`: the example `TopKOptimizerRule` also matches a
`Sort` with a fetch (see below). The `topk_plan` and `topk_invariants*`
tests failed without that change.
I checked that the optimizer tests fail without the fix. Results:
- `cargo test --profile ci -p datafusion-optimizer`: 901 + 26 + 5
passed, 0 failed.
- `cargo test --profile ci -p datafusion-sqllogictest --test
sqllogictests`: 522 of 522 files pass. No expected plan changes, because
the final plans are the same.
- `cargo fmt --all -- --check` and `cargo clippy --profile ci -p
datafusion-optimizer --all-targets -- -D warnings`: clean.
## Are there any user-facing changes?
Final plans are the same. Planning an `ORDER BY ... LIMIT` query without
an `OFFSET` takes one less optimizer pass.
A custom optimizer rule that runs after `PushDownLimit` and matches
`Limit` over `Sort` no longer sees that pattern in the first pass. It
sees a `Sort` with a fetch instead. On `main` such a rule also saw only
the `Sort` with a fetch from the second pass on. The `TopKOptimizerRule`
example in `datafusion/core/tests/user_defined/user_defined_plan.rs` had
this dependency, so it now also matches a `Sort` with a fetch.
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
1 parent 88306ff commit 5719930
3 files changed
Lines changed: 136 additions & 22 deletions
File tree
- datafusion
- core/tests/user_defined
- optimizer/src
- push_down_limit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
510 | 510 | | |
511 | 511 | | |
512 | 512 | | |
513 | | - | |
514 | | - | |
515 | | - | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
516 | 537 | | |
517 | 538 | | |
518 | 539 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
166 | | - | |
167 | | - | |
168 | | - | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
169 | 172 | | |
170 | | - | |
| 173 | + | |
171 | 174 | | |
172 | 175 | | |
173 | | - | |
174 | | - | |
175 | | - | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
176 | 184 | | |
177 | 185 | | |
178 | 186 | | |
| |||
660 | 668 | | |
661 | 669 | | |
662 | 670 | | |
663 | | - | |
| 671 | + | |
664 | 672 | | |
665 | 673 | | |
666 | 674 | | |
667 | | - | |
668 | | - | |
669 | | - | |
| 675 | + | |
| 676 | + | |
670 | 677 | | |
671 | 678 | | |
672 | 679 | | |
673 | 680 | | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
674 | 718 | | |
675 | 719 | | |
676 | 720 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1380 | 1380 | | |
1381 | 1381 | | |
1382 | 1382 | | |
1383 | | - | |
1384 | | - | |
1385 | | - | |
1386 | | - | |
1387 | | - | |
1388 | | - | |
| 1383 | + | |
| 1384 | + | |
| 1385 | + | |
| 1386 | + | |
1389 | 1387 | | |
1390 | | - | |
| 1388 | + | |
1391 | 1389 | | |
1392 | 1390 | | |
1393 | 1391 | | |
| |||
1402 | 1400 | | |
1403 | 1401 | | |
1404 | 1402 | | |
1405 | | - | |
| 1403 | + | |
1406 | 1404 | | |
1407 | 1405 | | |
1408 | 1406 | | |
| |||
1419 | 1417 | | |
1420 | 1418 | | |
1421 | 1419 | | |
| 1420 | + | |
| 1421 | + | |
| 1422 | + | |
| 1423 | + | |
| 1424 | + | |
| 1425 | + | |
| 1426 | + | |
| 1427 | + | |
| 1428 | + | |
| 1429 | + | |
| 1430 | + | |
| 1431 | + | |
| 1432 | + | |
| 1433 | + | |
| 1434 | + | |
| 1435 | + | |
| 1436 | + | |
| 1437 | + | |
| 1438 | + | |
| 1439 | + | |
| 1440 | + | |
| 1441 | + | |
| 1442 | + | |
| 1443 | + | |
| 1444 | + | |
| 1445 | + | |
| 1446 | + | |
| 1447 | + | |
| 1448 | + | |
| 1449 | + | |
| 1450 | + | |
| 1451 | + | |
| 1452 | + | |
| 1453 | + | |
| 1454 | + | |
| 1455 | + | |
| 1456 | + | |
| 1457 | + | |
| 1458 | + | |
| 1459 | + | |
| 1460 | + | |
| 1461 | + | |
| 1462 | + | |
| 1463 | + | |
| 1464 | + | |
| 1465 | + | |
| 1466 | + | |
| 1467 | + | |
| 1468 | + | |
| 1469 | + | |
| 1470 | + | |
1422 | 1471 | | |
1423 | 1472 | | |
1424 | 1473 | | |
| |||
0 commit comments