Skip to content

[opt](lance) push down LIMIT into Lance fragment scanners - #66608

Open
Jay-ju wants to merge 1 commit into
apache:branch-4.1from
Jay-ju:jay-ju/lance-scan-limit-pushdown
Open

[opt](lance) push down LIMIT into Lance fragment scanners#66608
Jay-ju wants to merge 1 commit into
apache:branch-4.1from
Jay-ju:jay-ju/lance-scan-limit-pushdown

Conversation

@Jay-ju

@Jay-ju Jay-ju commented Aug 10, 2026

Copy link
Copy Markdown

What problem does this PR solve?

Issue Number: N/A

Related PR: #65730, #66581

Problem Summary:

Ordinary Lance scans currently read every row of a fragment even when the
query only needs the first N rows (e.g. SELECT ... LIMIT 10). Lance applies
its own LIMIT after the scanner's filter, so the query LIMIT can be forwarded
to each fragment scanner and let it stop early, cutting IO and decode cost.

How it is fixed

  • thrift: add an optional TLanceFileDesc.limit.
  • FE (LanceScanNode): push the query limit into each fragment split via
    canPushDownLimit(), and surface lanceLimit in the explain output.
  • BE (lance_reader): forward it to the scanner through
    lance_scanner_set_limit for ordinary scans; vector search keeps its own
    top_k limit.

Correctness

The limit is pushed only when all predicates are already pushed into Lance
(no residual Doris conjunct). Otherwise Doris still re-filters the returned rows,
and truncating a fragment early could drop valid results.

OFFSET needs no special handling: Nereids' SplitLimit rewrites
Limit(limit, offset) into a global Limit(limit, offset) over a local
Limit(limit + offset, 0), and that local bound is what reaches the scan node.
So getLimit() already includes the offset; each fragment fetches up to
limit + offset rows and the upper global LIMIT still applies the offset and
the final bound. Per-fragment truncation is therefore always safe.

Behavior change

Query results are unchanged. Only the number of rows scanned per fragment is
reduced for LIMIT queries; the explain output shows an extra lanceLimit=N
line when the limit is pushed.

Release note

Push down LIMIT into Lance fragment scanners to reduce the rows scanned for
LIMIT / LIMIT ... OFFSET queries over Lance tables.

Check List (For Author)

  • Test
    • Unit Test (LanceThriftContractTest covers the limit round-trip and the no-limit case)
    • Manual test — SELECT * FROM <lance_tbl> LIMIT 10 returns 10 rows and EXPLAIN shows lanceLimit=10; a query with a non-pushable predicate keeps the limit out of the scan
  • Behavior changed:
    • No.
  • Does this need documentation?
    • No.

@Jay-ju
Jay-ju requested a review from yiguolei as a code owner August 10, 2026 07:51
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

Ordinary Lance scans read every row of a fragment even when the query
only needs the first N. Lance applies its LIMIT after the scanner's own
filter, so we can forward the query LIMIT to each fragment scanner and
let it stop early.

To stay correct we only push the LIMIT when all predicates are already
pushed into Lance (no residual Doris conjunct). Otherwise Doris still
filters the returned rows and an early truncation could drop valid
results. The upper LIMIT operator keeps enforcing the global bound
across fragments, so per-fragment truncation is always safe.

OFFSET needs no extra work: Nereids' SplitLimit rewrites
Limit(limit, offset) into a global Limit(limit, offset) over a local
Limit(limit + offset, 0), and that local bound is what reaches the scan
node. So getLimit() already includes the offset and each fragment simply
fetches up to limit + offset rows.

- thrift: add optional TLanceFileDesc.limit
- FE: set it in LanceScanNode.setScanParams via canPushDownLimit()
  (hasLimit() and no residual conjunct); also surface it in explain
- BE: forward it via lance_scanner_set_limit for ordinary scans
  (vector search keeps its own top_k limit)
@Jay-ju
Jay-ju force-pushed the jay-ju/lance-scan-limit-pushdown branch from 5ba2501 to 7fb0a61 Compare August 10, 2026 08:56
@Jay-ju

Jay-ju commented Aug 10, 2026

Copy link
Copy Markdown
Author

Thank you for your contribution to Apache Doris. Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

Thanks for the reminder! I've just updated the PR description with the
problem background, the fix, the correctness reasoning (including how
OFFSET is handled) and the test plan. Please let me know if anything is
still unclear — happy to add more detail. 🙏

@Jay-ju

Jay-ju commented Aug 10, 2026

Copy link
Copy Markdown
Author

@hello-stephen Please take a look when you have time, thanks.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants