Skip to content

branch-4.2: [Improvement](scan) Push down LIMIT to segment iterator (#61713 #62222 #64285 #64479 #68804) - #68788

Merged
yiguolei merged 4 commits into
apache:branch-4.2from
csun5285:pick-4.2/segment-limit-pushdown
Oct 14, 2026
Merged

yiguolei merged 4 commits into
apache:branch-4.2from
csun5285:pick-4.2/segment-limit-pushdown

Conversation

@csun5285

@csun5285 csun5285 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

pick #61713 and #62222 (partial: no shared scan LIMIT counter, no key-expression pushdown for tables merged on read), remove enable_common_expr_pushdown as #64285 and #64479 do on master, and pick #68804

@csun5285
csun5285 requested a review from yiguolei as a code owner October 8, 2026 10:48
@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?

@csun5285

csun5285 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 78.49% (1960/2497)
Line Coverage 65.46% (35323/53964)
Region Coverage 53.07% (33989/64043)
Branch Coverage 55.86% (10480/18762)

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 100.00% (2/2) 🎉
Increment coverage report
Complete coverage report

@csun5285
csun5285 force-pushed the pick-4.2/segment-limit-pushdown branch from ddf7589 to e63cc1e Compare October 9, 2026 07:08
@csun5285

csun5285 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 78.49% (1960/2497)
Line Coverage 65.43% (35311/53964)
Region Coverage 53.08% (33991/64043)
Branch Coverage 55.88% (10485/18762)

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 100.00% (2/2) 🎉
Increment coverage report
Complete coverage report

@csun5285
csun5285 force-pushed the pick-4.2/segment-limit-pushdown branch from e63cc1e to 0d098c1 Compare October 12, 2026 09:35
@csun5285

Copy link
Copy Markdown
Contributor Author

run buildall

@csun5285 csun5285 changed the title branch-4.2: [Improvement](scan) Push down LIMIT to segment iterator (#61713 #62222) branch-4.2: [Improvement](scan) Push down LIMIT to segment iterator (#61713 #62222 #64285 #64479 #68804) Oct 12, 2026
@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 21.05% (4/19) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 36.84% (7/19) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 0.99% (7/710) 🎉
Increment coverage report
Complete coverage report

@yiguolei

Copy link
Copy Markdown
Contributor

/review

BiteTheDDDDt and others added 4 commits October 13, 2026 14:09
)

This pull request implements a general limit pushdown optimization for
storage-layer scans on DUP_KEYS and UNIQUE_KEYS (with Merge-on-Write,
MOW) tables. The optimization ensures that LIMIT clauses are enforced
after filtering, preventing the scan from returning fewer rows than
requested when filters are present. The change is carefully scoped to
avoid interfering with existing optimizations (like TopN or storage
merge paths) and includes comprehensive regression tests to validate
correctness across various scenarios.

**General Limit Pushdown Implementation:**

* Added a `general_read_limit` parameter to the storage layer
(`TabletReader`, `RowsetReaderContext`, and `VCollectIterator`) to
enforce a post-filter row limit in non-merge scan paths for DUP_KEYS and
UNIQUE_KEYS with MOW. The iterator now tracks the number of rows
returned and stops reading once the limit is reached, ensuring correct
LIMIT semantics even in the presence of filters.
[[1]](diffhunk://#diff-4d3f2b2a64e1dac3bc076994d3cd9a6c569f7a8c6ad5bc69425188ffccb4266bR494-R503)
[[2]](diffhunk://#diff-4d3f2b2a64e1dac3bc076994d3cd9a6c569f7a8c6ad5bc69425188ffccb4266bR518-R531)
[[3]](diffhunk://#diff-88c1391d53c67a5ab8475c103bb743a2a80cc282e0c752e8fc52b7aaeb2e0e35L95-R102)
[[4]](diffhunk://#diff-88c1391d53c67a5ab8475c103bb743a2a80cc282e0c752e8fc52b7aaeb2e0e35R258-R294)
[[5]](diffhunk://#diff-b7a74d17e270719c904e8c31fb1308cb82ec0b89459a245dce9cb5505021ac63R353-R358)
[[6]](diffhunk://#diff-0025ea2b49c400923787cd77990575f94ba5d925efc95cb16336fbaec92ebfb1R106-R109)
[[7]](diffhunk://#diff-979b99e23ca7a4bb73f5beaa00270494778545296929a3de64b2e87dc8299cd6R202-R203)
[[8]](diffhunk://#diff-48c14ad6bacc0a0ed497ad0cde34756af85aa98dcb7dc909609a288a503be0c9R216-R220)

**Testing and Validation:**

* Added a new regression test suite
(`test_general_limit_pushdown.groovy`) covering DUP_KEYS, UNIQUE_KEYS
(MOW), AGG_KEYS, and MOR tables. The tests verify correct row counts and
output for LIMIT queries with and without filters, as well as negative
cases where the optimization must not apply.
* Added corresponding expected output results for the new tests.

(cherry picked from commit afb1601)

Co-Authored-By: Claude Fable 5.1 <[email protected]>
…he#62222)

This pull request introduces significant improvements to scan operator
logic, particularly enhancing the correctness and efficiency of LIMIT
and predicate pushdown in OLAP scans. The main changes include a robust
mechanism for sharing LIMIT counters among scanners, more precise
control over predicate pushdown, and stricter validation for residual
predicates. These changes help ensure that queries with LIMIT and TopN
semantics return accurate results and avoid incorrect or inefficient
execution paths.

**Improvements to LIMIT handling and scanner coordination:**

- Introduced a shared atomic counter (`_shared_scan_limit`) for LIMIT
queries, allowing all scanners to collectively respect the global row
limit. Scanners now check and update this counter to stop early when the
LIMIT is reached, preventing over-scanning and improving efficiency.
TopN scans bypass this mechanism as required.
[[1]](diffhunk://#diff-7fe2376680f9ab835053a0a93c7f9458e6d526acfc180318be0ad936e0c2dedeR50-R55)
[[2]](diffhunk://#diff-7fe2376680f9ab835053a0a93c7f9458e6d526acfc180318be0ad936e0c2dedeR135-R143)
[[3]](diffhunk://#diff-7fe2376680f9ab835053a0a93c7f9458e6d526acfc180318be0ad936e0c2dedeR177-R183)
[[4]](diffhunk://#diff-7fe2376680f9ab835053a0a93c7f9458e6d526acfc180318be0ad936e0c2dedeR196)
[[5]](diffhunk://#diff-9c777f60e1d076777f042a0b702967bd5864bcfe9ee1eee9b456bb67c6fdf817R266-R269)
[[6]](diffhunk://#diff-a45108106b12759815ac5991d56a9d6ead7ffb9c915fb0cf7b6f0b8123b30262R71)
[[7]](diffhunk://#diff-a45108106b12759815ac5991d56a9d6ead7ffb9c915fb0cf7b6f0b8123b30262R239)
[[8]](diffhunk://#diff-a45108106b12759815ac5991d56a9d6ead7ffb9c915fb0cf7b6f0b8123b30262L283-R285)
[[9]](diffhunk://#diff-3c4794a864169735a628d8fcb1de986523a9516aad318aa0a893913f4ce031d4R1043-R1050)

- Adjusted scanner and scan local state interfaces to support the new
shared LIMIT mechanism, including new virtual methods and atomic counter
management.
[[1]](diffhunk://#diff-a45108106b12759815ac5991d56a9d6ead7ffb9c915fb0cf7b6f0b8123b30262R71)
[[2]](diffhunk://#diff-a45108106b12759815ac5991d56a9d6ead7ffb9c915fb0cf7b6f0b8123b30262R239)
[[3]](diffhunk://#diff-a45108106b12759815ac5991d56a9d6ead7ffb9c915fb0cf7b6f0b8123b30262L283-R285)

**Predicate pushdown and validation enhancements:**

- Refactored `_should_push_down_common_expr` to require the expression
as an argument and added logic to ensure only eligible expressions are
pushed down, considering storage merge requirements and key columns.
[[1]](diffhunk://#diff-780cdfbcbb1d30adb63c61521e6b681cad347bebeaa5332a781d05bb51543527L46-R46)
[[2]](diffhunk://#diff-235cc51f4698ebf0ebe796e7253912907bb7c1caf4efcac92538ac1ff2eac171L104-R107)
[[3]](diffhunk://#diff-3ddc75656071d9c0e6b0be450e152a1c94559f7e70ea820e7f0c80a7078e3292L465-R547)
[[4]](diffhunk://#diff-3c4794a864169735a628d8fcb1de986523a9516aad318aa0a893913f4ce031d4L313-R313)

- Added a validation step (`validate_residual_scan_conjuncts`) to
prevent unsupported residual predicates (like SEARCH or disabled MATCH)
and to ensure correctness when using COUNT_ON_INDEX pushdown.

**OLAP scan and segment limit logic improvements:**

- Improved the logic for pushing down segment limits and TopN
optimizations, ensuring that such optimizations are only enabled when
all necessary conditions are met (e.g., no residual predicates, no
runtime filters, storage does not require merging). The code now asserts
correct states and disables shared LIMITs for TopN scans as appropriate.
[[1]](diffhunk://#diff-4d3f2b2a64e1dac3bc076994d3cd9a6c569f7a8c6ad5bc69425188ffccb4266bL446-R502)
[[2]](diffhunk://#diff-3ddc75656071d9c0e6b0be450e152a1c94559f7e70ea820e7f0c80a7078e3292R1155-R1156)

**Code cleanup and maintenance:**

- Removed unused or obsolete logic related to filter block conjuncts and
the old limit quota acquisition method.
[[1]](diffhunk://#diff-4d3f2b2a64e1dac3bc076994d3cd9a6c569f7a8c6ad5bc69425188ffccb4266bL90)
[[2]](diffhunk://#diff-0c9a817d45d8130ea3211189e1321d1275e22fd4a9a3fac2bd707b1cfeefa5e5L115-L135)

**Dependency and include updates:**

- Added missing includes for expression types to support new logic.


```sql
CREATE DATABASE IF NOT EXISTS lm_bench;
USE lm_bench;

DROP TABLE IF EXISTS lm_wide_fact_10m;
CREATE TABLE lm_wide_fact_10m (
    id BIGINT NOT NULL,
    filter_key INT NOT NULL,
    sort_key BIGINT NOT NULL,
    metric BIGINT NOT NULL,
    payload_a VARCHAR(1024) NOT NULL,
    payload_b VARCHAR(1024) NOT NULL,
    long_payload_a VARCHAR(4096) NOT NULL,
    long_payload_b VARCHAR(4096) NOT NULL
)
ENGINE=OLAP
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 32
PROPERTIES (
    "replication_num" = "1"
);

INSERT INTO lm_wide_fact_10m
SELECT
    number AS id,
    CAST(number % 1000000 AS INT) AS filter_key,
    (number * 2654435761) % 1000000007 AS sort_key,
    (number * 1103515245 + 12345) % 2147483647 AS metric,
    REPEAT(CONCAT('payload_a_', CAST(number AS STRING), '_',
        CAST((number * 1315423911) % 1000000007 AS STRING), '_'), 16) AS payload_a,
    REPEAT(CONCAT('payload_b_', CAST(number AS STRING), '_',
        CAST((number * 2654435761) % 1000000009 AS STRING), '_'), 16) AS payload_b,
    REPEAT(CONCAT('long_payload_a_', CAST(number AS STRING), '_',
        CAST((number * 48271) % 2147483647 AS STRING), '_'), 64) AS long_payload_a,
    REPEAT(CONCAT('long_payload_b_', CAST(number AS STRING), '_',
        CAST((number * 69621) % 2147483629 AS STRING), '_'), 64) AS long_payload_b
FROM numbers("number" = "10000000");

ANALYZE TABLE lm_wide_fact_10m WITH SYNC;


-- ============================================================
-- Case 1: TopN next + LIMIT + high-filter-rate pushed-down predicate
-- Expected observation:
--   filter_key is a narrow pushed-down predicate and keeps about 1% rows.
--   TopN works on narrow metric/sort_key, while long strings are final output.
-- ============================================================

SELECT
    id,
    filter_key,
    metric,
    sort_key,
    payload_a,
    payload_b,
    long_payload_a,
    long_payload_b
FROM lm_wide_fact_10m
WHERE filter_key < 10000
ORDER BY metric DESC, sort_key ASC
LIMIT 10;
-- Result: 0.12 sec -> 0.10 sec

-- ============================================================
-- Case 2: Normal scan + LIMIT
-- Expected observation:
--   No ORDER BY and no predicate. This isolates ordinary scan limit behavior
--   while still returning long string columns.
-- ============================================================

SELECT
    id,
    filter_key,
    metric,
    sort_key,
    payload_a,
    payload_b,
    long_payload_a,
    long_payload_b
FROM lm_wide_fact_10m
LIMIT 10;
-- Result: 0.06 sec -> 0.01 sec

-- ============================================================
-- Case 3: Normal scan + LIMIT + high-filter-rate pushed-down predicate
-- Expected observation:
--   No ORDER BY. This isolates scan predicate + limit behavior with long
--   string result columns and a predicate that filters about 99% of rows.
-- ============================================================

SELECT
    id,
    filter_key,
    metric,
    sort_key,
    payload_a,
    payload_b,
    long_payload_a,
    long_payload_b
FROM lm_wide_fact_10m
WHERE filter_key < 10000
LIMIT 10;
-- Result: 0.07 sec -> 0.02 sec

```

---------

Co-authored-by: Copilot <[email protected]>

### Backport notes (branch-4.2)

Partial pick. This branch keeps the enable_common_expr_pushdown switch and has no
shared scan LIMIT counter (apache#61617 was reverted here by apache#62191), so:

- Not picked: scanner.cpp/.h, scanner_context.cpp/.h and scanner_scheduler.cpp (the
  shared counter), ScanLocalState::shared_scan_limit_ptr(), and the
  collection_statistics_test.cpp hunk (the file does not exist on this branch).
- Not picked: pushing key-only common expressions into SegmentIterator for tables
  merged on read (AGG, UNIQUE without merge-on-write). _should_push_down_common_expr()
  keeps the rule this branch had: switch on, no storage merge, expression acts on a
  slot (VExpr::is_acting_on_a_slot stays). The follow-up fix apache#63279 was only needed by
  that pushdown and is not picked either.
- RuntimeState keeps enable_common_expr_pushdown() and
  enable_common_expr_pushdown_for_inverted_index(). The OlapScanner assertion that
  every slot-based conjunct was pushed down only runs with the switch on. The thrift
  comment on field 107 is unchanged.
- Not picked: validate_residual_scan_conjuncts (the SEARCH / MATCH / COUNT_ON_INDEX
  residual-predicate errors). This branch still has enable_common_expr_pushdown, and
  fuzzy sessions and existing tests such as test_compound_1 turn it off. Those
  predicates then stay in the scanner, which this branch evaluates correctly, so the
  errors would reject valid queries. On master the switch is gone (apache#64285) and the
  tests no longer turn it off (apache#64479).
- Key TopN on tables merged on read keeps the behavior this branch had: without
  runtime filters the scanner sets read_orderby_key* and keeps _limit = sort_limit, so
  it stops after its own N key-ordered rows (master resets _limit to -1 for them).
  Storage TopN (read_orderby_key_limit) is only set for physical DUP_KEYS and
  merge-on-write UNIQUE_KEYS tablets; for a MOR table read as DUP the reader does not
  prepare the key columns that VCollectIterator::_topn_next() compares, so that case
  falls back to reading every row.
- TQueryOptions field id is 231; 219 and 230 are already used on this branch. apache#68785
  gives master the same id.
- SessionVariable keeps the VariableMgr.VarAttr annotation style. The "Legacy session
  variable" comment is not added.
- segment_iterator.cpp: the count fast path check stays ahead of the read_limit EOF
  check, the shrink_char_type_column_suffix_zero() call does not exist here, and
  count_on_index_fastpath's has_read_limit now reads _opts.read_limit (the SNII
  backport apache#68584 pinned it to false because this branch had no read_limit yet).
- segment_iterator_limit_opt_test builds its Schema from (columns, col_ids); this
  branch has no Schema(TabletSchemaSPtr) constructor.

Verified on this branch: BE (ASAN) and FE build with 0 checkstyle violations; the 23
related BE UT suites (183 cases, including SegmentIteratorLimitOptTest) pass; the
regression suites query_p0/limit (18 suites), test_search_slash_in_term,
infer_predicate and test_variant_predefine_index_type pass against the picked .out
files. Without validate_residual_scan_conjuncts, the suites that failed in CI with
residual-predicate errors pass with enable_common_expr_pushdown both off and on, except
test_multi_tokenize_index_not_built and test_search_score_topn_predicates, which fail
with the switch off on this branch's own errors (E-6001, SearchExpr without inverted
index), not on anything from this pick. The kept scanner-level stop was measured on a Release build: on an AGG table
built from 4M input rows, ORDER BY key LIMIT 10 scans 131k rows in 19 ms; the same
binary with master's behavior (_limit reset to -1 for tables merged on read) scans 2.9M
rows in 114 ms. A MOR UNIQUE table goes 98k rows / 20 ms against 2.2M rows / 84 ms.
Results are identical.

(cherry picked from commit 28cc2bf)

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

# Conflicts:
#	gensrc/thrift/PaloInternalService.thrift

# Please enter the commit message for your changes. Lines starting
# with '#' will be kept; you may remove them yourself if you want to.
# An empty message aborts the commit.
#
# Author:    Pxl <[email protected]>
#
# interactive rebase in progress; onto cabcf45
# Last commands done (2 commands done):
#    pick f6d4a8d4292 # [Improvement](scan) update limit pushdown to tablet reader (apache#61713)
#    pick 428d832fcb0 # [Improvement](scan) support push down limit to segment iterator (apache#62222)
# No commands remaining.
# You are currently rebasing branch 'pick-4.2/segment-limit-pushdown' on 'cabcf45c1af'.
#
# Changes to be committed:
#	modified:   be/src/exec/operator/mock_scan_operator.h
#	modified:   be/src/exec/operator/olap_scan_operator.cpp
#	modified:   be/src/exec/operator/olap_scan_operator.h
#	modified:   be/src/exec/operator/scan_operator.cpp
#	modified:   be/src/exec/operator/scan_operator.h
#	modified:   be/src/exec/scan/olap_scanner.cpp
#	modified:   be/src/runtime/runtime_state.h
#	modified:   be/src/storage/iterator/vcollect_iterator.cpp
#	modified:   be/src/storage/iterator/vcollect_iterator.h
#	modified:   be/src/storage/iterators.h
#	modified:   be/src/storage/rowset/beta_rowset_reader.cpp
#	modified:   be/src/storage/rowset/rowset_reader_context.h
#	modified:   be/src/storage/segment/segment_iterator.cpp
#	modified:   be/src/storage/segment/segment_iterator.h
#	modified:   be/src/storage/tablet/tablet_reader.cpp
#	modified:   be/src/storage/tablet/tablet_reader.h
#	new file:   be/test/storage/segment/segment_iterator_limit_opt_test.cpp
#	modified:   fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
#	modified:   gensrc/thrift/PaloInternalService.thrift
#	new file:   regression-test/data/query_p0/limit/test_unified_limit_pushdown.out
#	modified:   regression-test/data/search/test_search_slash_in_term.out
#	modified:   regression-test/data/variant_p0/predefine/test_predefine_type_index.out
#	modified:   regression-test/suites/query_p0/limit/test_general_limit_pushdown.groovy
#	new file:   regression-test/suites/query_p0/limit/test_unified_limit_pushdown.groovy
#	modified:   regression-test/suites/search/test_search_slash_in_term.groovy
#	modified:   regression-test/suites/variant_p0/predefine/test_predefine_type_index.groovy
#
# Untracked files:
#	be/dict/kuromoji/
#	ui/tsconfig.app.tsbuildinfo
#	ui/tsconfig.node.tsbuildinfo
#
### What problem does this PR solve?

Backport to branch-4.2 of how master dropped enable_common_expr_pushdown and
enable_common_expr_pushdown_for_inverted_index (apache#62222, apache#64285, apache#64479).

- BE: RuntimeState::enable_common_expr_pushdown() and
  enable_common_expr_pushdown_for_inverted_index() are gone. Tables not merged on
  read always push slot-based conjuncts into SegmentIterator, the OlapScanner check
  that every such conjunct was pushed runs whenever segment LIMIT can be pushed, and
  SegmentIterator no longer returns early from
  _check_all_conditions_passed_inverted_index_for_column.
- BE: validate_residual_scan_conjuncts from apache#62222 comes back. A residual SEARCH, a
  residual MATCH with enable_match_without_inverted_index off, and COUNT_ON_INDEX with
  residual scan conjuncts now fail with INVALID_ARGUMENT. The SEARCH and MATCH messages
  also say that tables merged on read do not push expressions on this branch.
- FE: the two session variables are removed (apache#64285). VariableMgr takes
  REMOVED_SESSION_VAR_NAMES from apache#63744, listing only these two names, so SET,
  SET GLOBAL, SET_VAR and SELECT @@ on them still succeed and do nothing.
- FE: fuzzy mode now randomizes enable_segment_limit_pushdown (apache#64479).
- thrift: field 65 is marked deprecated. No field id changes.
- Regression cases follow apache#64479: a setting of either removed variable becomes
  enable_segment_limit_pushdown with the same value; two settings in one SET_VAR or on
  two adjacent SET lines become one, taking the enable_common_expr_pushdown value when
  they differ. 214 files take apache#64479's hunks without its trailing-whitespace cleanup,
  57 files that apache#64479 misses or that differ here are rewritten by the same rules (one
  file gets both), and 12 files keep the removed names exactly as master has them today.

Upgrade BE before FE. An old BE behind a new FE no longer receives
enable_common_expr_pushdown, reads it as false, and keeps conjuncts in the scanner
until it is upgraded.

Verified on this branch (ASAN BE):
- FE and BE build, 0 checkstyle violations, BE changes clean under clang-format 16.
- FE UT SessionVariablesTest, VariableMgrTest and SessionVariableBackendSelectionTest
  (42 cases) pass. The 23 related BE UT suites (183 cases, including the SegmentIterator
  apply-index-expr, short-circuit, expr-zonemap, candidate-pushdown and no-need-read-data
  suites) pass.
- SET, SET GLOBAL, SET_VAR and SELECT @@ on both removed names succeed; SELECT @@ returns
  an empty string, SHOW VARIABLES lists neither, and an unknown name still fails.
- With enable_segment_limit_pushdown set globally to false and then true:
  query_p0/limit (18 suites), test_search_slash_in_term, test_variant_predefine_index_type,
  infer_predicate, and 69 more suites (the ones that failed on residual predicates in this
  PR's earlier CI, the ones that turned the old switch off, the ones that still set the
  removed names, and migrated SNII and gram cases) pass, except
  test_search_score_topn_predicates. That suite fails the same way in both runs, and CI
  mutes it on other branch-4.2 PRs (apache#68213, apache#68774): the FE reports the score() usage
  error before the LIMIT overflow error the case expects.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
### What problem does this PR solve?

Pick of master PR apache#68804 (be261b7, still in review) to branch-4.2.

A merge-on-read UNIQUE table listed in read_mor_as_dup_tables is read without merging, so
OlapScanLocalState::_storage_no_merge() is true for it, but
TabletReader::_init_orderby_keys_param() sets up the key columns that
VCollectIterator::_topn_next() compares only for DUP_KEYS and merge-on-write tablets. The
apache#62222 pick on this branch already keeps segment LIMIT off for a key TopN on such a table,
through a separate condition next to can_push_down_segment_limit. This commit switches to
apache#68804's form of the same rule:

- OlapScanner folds storage TopN support into can_push_down_segment_limit, so the
  DORIS_CHECK that guards a pushed LIMIT also covers it. The behavior does not change: the
  scan returns every row and the TopN operator above sorts them.
- TabletReader::_init_orderby_keys_param() returns INTERNAL_ERROR when storage TopN is
  requested but no order-by key columns are set up, so a wrong pushdown fails the query at
  reader init instead of aborting the BE in _topn_next().

Branch-4.2 notes: be/test/storage/tablet_reader_test.cpp does not exist on this branch, so it
is added with only the two TopN cases from apache#68804 and a fixture that sets _return_columns
(this branch has no ReadSchema). The regression case and its .out are taken as is.

Verified on this branch (ASAN BE):
- BE builds, and the changes are clean under clang-format 16.
- 24 related BE UT suites (185 cases) pass, including the two new TabletReaderTest cases:
  storage TopN on a merge-on-read tablet, or with an empty key prefix, fails
  _init_orderby_keys_param() with INTERNAL_ERROR.
- test_read_mor_as_dup_key_topn, test_read_mor_as_dup and query_p0/limit (18 suites) pass
  with enable_segment_limit_pushdown set globally to false and then true.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@csun5285
csun5285 force-pushed the pick-4.2/segment-limit-pushdown branch from 0d098c1 to 7cce838 Compare October 13, 2026 06:09
@csun5285

Copy link
Copy Markdown
Contributor Author

run buildall

@csun5285

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

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.

Review complete after three rounds of full review and risk rechecks. Every final-round reviewer returned NO_NEW_VALUABLE_FINDINGS; the final changed-file/candidate sweep has no unresolved point.

Static review of PR #68788 at 7cce8383538bac3cbc970c7ad307d03b2b90d061, covering all 299 changed files. Two new findings are attached inline: a P1 incomplete condition-cache result can make later queries omit matching rows, and a P2 residual guard rejects valid COUNT(*) queries with slotless volatile predicates. No existing inline findings were present, so there are no existing blocking comment IDs.

The end-to-end flow reviewed is FE session/query options → scan predicate normalization → scanner reader parameters → tablet/rowset readers → segment filtering and local budget → lazy materialization → scanner and global LIMIT/TopN. Two normal reviewers cover backend and FE/protocol/regressions; a separate reviewer covers LIMIT/TopN risks. Both findings are independently verified and deduplicated.

Critical checkpoint Conclusion
Goal and proof General LIMIT and key TopN budgets now follow storage predicates/common expressions. Ordinary selection implements the goal, but cache completeness and valid slotless COUNT execution have the attached defects. Added tests were inspected, not executed.
Scope and clarity Production changes are focused on normalization, budget propagation, expression-switch removal and variable compatibility. Removed scanner-filter plumbing uses existing storage expression contexts.
Concurrency and locks Budgets/counters are iterator-local; expression contexts are cloned. All planned runtime filters veto segment budgets, including late arrivals. No new threads, shared mutable budgets, heavy locked operations or lock inversion found.
Lifecycle and static initialization Existing rowset/column ownership and exception boundaries are retained. Consumed Block schema counts remain valid. EOF child blocks are discarded by merge readers; their unsafe cache-publication side effect is the inline finding. No new ownership cycle or cross-TU initialization dependency.
Configuration and dynamic behavior New session switch defaults true, is forwarded and takes effect during scanner setup without restart. Explicit false disables segment LIMIT while retaining mandatory expression/index evaluation. Removed names support SET, @@ and hint compatibility no-ops.
Symbols, formats and rolling upgrades Renamed C++ members are internal; no stored data format changes. Absent new option defaults true for old FE/new BE. Removal of legacy execution options assumes the intended BE-before-FE upgrade order.
Thrift/protobuf definitions TQueryOptions field65 retains meaning and field107 remains reserved. New231 is optional, unused on release3.1/4.0/4.1/4.2 and identical on master. No introduced collision or required field. Unrelated shipped-branch mismatches were not treated as new findings.
Parallel paths DUP/MOW and MOR-as-DUP general limits, native MOR/AGG post-merge limits, cloud/shared-nothing readers, ASC/DESC, deletes, count shortcuts, ANN and virtual/nested columns were traced. MOR-as-DUP storage TopN is explicitly disabled.
Special conditions Positive bound, no residuals, no planned runtime filters and no storage merge gate irreversible budgets. FE pushes valid key-prefix sort info/limit together. Invalid residual SEARCH and disabled slow MATCH return errors. The COUNT_ON_INDEX residual guard is overbroad for valid slotless volatile predicates (P2).
Test coverage New helper/tablet units and regression suites cover eligibility, filtered row counts, reverse suffix slicing, index/no-index predicates, MOW deletes, buckets, offsets and MOR versions. Missing cache warm-then-unrestricted coverage exposes the P1; missing indexed COUNT(*) with one slotless volatile filter exposes the P2.
Modified expected results All changed regression paths and added golden rows were inspected against inserts, predicates and limits; no additional incorrect expected value found. Generation provenance and passing execution cannot be verified statically.
Observability Existing predicate/raw-read/cache metrics and cache hit/insert VLOGs cover these paths. No new distributed operation or hot-loop INFO logging requires additional instrumentation.
Transactions and persistence Query-visible versions and delete-bitmap binding are unchanged. No transaction or failover protocol change. Session/global reflection, EditLog replay and checkpoint handling were traced; removed names are skipped and the new option is persisted through existing mechanisms.
Writes, atomicity and crashes No data-write, publish or storage-metadata mutation path changes. Existing reader cancellation/cleanup remains; no additional write leak, crash atomicity or deadlock defect found.
FE-BE send paths Ordinary coordinator, cloud coordinator, forwarding and point-query paths carry query options. Constant folding has no SegmentIterator and does not require this LIMIT option.
Performance and memory The budget reduces lazy nonpredicate reads; raw reads are capped only after remaining evaluation is ruled out. Existing tracked column implementations handle shrink/cut. No new bulk allocation or redundant hot-loop work found.
Additional invariants Version visibility, MoW deletion, nullable/const columns, hidden delete fields and selector alignment have no additional substantiated defect. The two substantiated exceptions are cache completeness and slotless COUNT rejection, reported inline.

No additional user focus was supplied. This is a static-only review: no builds or tests were run, no source files were edited, and submodules remain uninitialized as required by AGENTS.md.

if (_count_emit_shortcut) {
return _emit_count_shortcut_batch(block);
}
if (_opts.read_limit > 0 && _rows_returned >= _opts.read_limit) {

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.

[P1] Keep LIMIT exhaustion from publishing a partial condition cache

This new EOF also reaches next_batch()'s unconditional condition-cache insertion at lines 3098-3107, although the segment has not been fully evaluated. With enable_condition_cache=true, topn_filter_ratio=0, and a DUP table containing an overlapping multi-segment rowset, WHERE abs(v) > 0 ORDER BY k LIMIT 1 enables storage TopN. VMergeIterator::_next_batch() advances each consumed child block and requests another batch before the scanner can stop, so this branch publishes the cache. Only rows retained by LIMIT were marked after _apply_read_limit_to_selected_rows(); other 2048-row granules remain false. A later query with the same predicate and a larger/no LIMIT reuses the predicate digest and discards those granules, silently omitting matching rows. Skip cache publication when EOF is caused by the read budget, or conservatively mark unvisited/discarded granules as possibly matching.

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.

master 存在这个问题,先在master修复,然后pick 到 4.2

}
}

if (push_down_agg_type == TPushAggOp::COUNT_ON_INDEX && !conjuncts.empty()) {

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.

[P2] Fall back for COUNT queries with slotless volatile predicates

On a DUP/MOW table with an inverted index, SELECT count(*) FROM t WHERE random() < 2 reaches this guard with the default enable_count_on_index_pushdown=true:

Aggregate(count(*))
  Filter(random() < 2)
    Scan(indexed_table)

AggregateStrategies.COUNT_ON_INDEX_WITHOUT_PROJECT accepts this because COUNT(*) has no input slots, and visitPhysicalFilter() puts the predicate on the COUNT_ON_INDEX scan. A single random() is neither folded nor extracted into a separate Project. In BE it is nonconstant and has no slot, so normalization retains it in _conjuncts instead of pushing it into SegmentIterator. The previous COUNT_ON_INDEX path emitted one row per source record and the scanner safely evaluated this predicate before the upper COUNT; since random() returns values in [0,1), this example returns the full row count. The new unconditional rejection turns that valid query into InvalidArgument. Preserve safe row-shaped residual evaluation or fall back to ordinary scanning/counting before enabling this pushdown.

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.

master 存在这个问题,先在master修复,然后pick 到 4.2

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 21.05% (4/19) 🎉
Increment coverage report
Complete coverage report

yiguolei pushed a commit that referenced this pull request Oct 13, 2026
…port (#68212)

Cherry-pick of #62737 to branch-4.2

[feature](runtime filter) Add decoupled runtime filter support

Adaptations for branch-4.2:
* `RuntimeFilterConsumer` keeps branch-4.2's `wait_infinitely`
computation (no BITMAP_FILTER term) and wraps it in the new
`desc->__isset.wait_time_ms` branch.
* `RuntimeFilterTranslator`/generator call branch-4.2's
`PushDownContext.createPushDownContextForHashJoin` factory instead of
master's unified `createPushDownContext`.
* The three new session variables use branch-4.2's
`@VariableMgr.VarAttr`; `runtimeFilterWaitInfinitely` becomes public as
upstream requires.

Rebased onto the latest branch-4.2 (Variant relational-ops RF guards
merged into `RuntimeFilterPushDownVisitor`, new Variant RF unit tests
preserved in `RuntimeFilterTest`).

CI fixes:
* shape_check baselines regenerated with `-forceGenOut` for every case
that failed in P0 regression: tpcds_sf100 4/11/31/32/74/92 (shape,
no_stats_shape, noStatsRfPrune, rf_prune), tpcds_sf1000
4/11/24/30/32/81/92 (shape, hint), tpcds_sf1000_constraints
4/11/24/30/81, tpcds_sf10t_orc 4/11/18/31/32/54/74/92, tpch_sf1000
q2/q8/q9/q17/q18 (shape, shape_no_stats, nostats_rf_prune, rf_prune),
tpch_sf1000_constraints q9 and the dphyper variants tpcds_sf1000
query54/query72 and tpch_sf1000 q8/q9/q18. Every regenerated file
differs from the branch-4.2 baseline only in runtime-filter annotations
(build RF target lists and applied RFs); plan shapes, join distributions
and predicates are untouched. For the dphyper files the regenerated
lines match the mismatches reported by CI one by one.
* `RuntimeFilterTest#testDecoupledRuntimeFilter` now asserts the
branch-4.2 observable behaviour instead of a separate `exprOrder == -1`
filter. On branch-4.2
`RuntimeFilterContext#getRuntimeFilterBySrcAndType` identifies a filter
by (srcExpr, type, builderNode) only, so the decoupled RF is merged into
the standard RF of the builder join (master dedups per target scan
including exprOrder and keeps them apart). The test checks, with and
without the feature, that the join2 RF additionally targets
`supplier.s_suppkey` and that the join1 RF becomes non-blocking when the
decoupled RF is preferred.

Testing:
* `run-fe-ut.sh --run
org.apache.doris.nereids.postprocess.RuntimeFilterTest` -> Tests run:
33, Failures: 0, Errors: 0.
* regression suite `nereids_syntax_p0/decoupled_runtime_filter` passes.
* every shape_check suite touching a regenerated baseline passes locally
(tpcds_sf100/sf1000/sf1000_constraints/sf10t_orc,
tpch_sf1000/tpch_sf1000_constraints, including the dphyper suites). The
decoupled RFs are reproduced because the local cluster has the same
loaded tpcds/tpch data as CI.
* Unrelated red checks from the previous run: BE UT failures were
`FileCacheProfileReporterTest` (x3) and
`RegexGramRecallTest.DynamicPatternRegexExecution`, which are known
flaky and are muted in TeamCity (same tests muted on other PR builds),
and the NonConcurrent `FE IMAGE GENERATED FAIL / CHECKPOINT FAIL`
failure is infra-wide (identical failure on #68788).

---------

Signed-off-by: Aurélien Pupier <[email protected]>
Co-authored-by: Aurélien Pupier <[email protected]>
Co-authored-by: Copilot <[email protected]>
@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 1.79% (4/224) 🎉
Increment coverage report
Complete coverage report

@yiguolei
yiguolei merged commit ccb49f1 into apache:branch-4.2 Oct 14, 2026
27 of 30 checks passed
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.

4 participants