Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions be/src/exec/operator/olap_scan_operator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,17 @@ static bool contains_expr_node_type(const VExprSPtr& expr, TExprNodeType::type n
});
}

// Find MATCH recursively; ones nested in AND / OR / NOT count too.
static bool is_match_expr(const VExprSPtr& expr) {
DORIS_CHECK(expr != nullptr);
if (expr->node_type() == TExprNodeType::MATCH_PRED ||

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] Gate this on index execution, not only on expression shape. With enable_inverted_index_query=false (or a MATCH column without an iterator), this still returns true even though SegmentIterator skips index evaluation and runs the supported row fallback. On a single large segment, the old row-count builder can split that expensive fallback across scanners, while this route creates exactly one scanner and serializes it; on many segments it also triggers the unbounded population noted separately. Row-only wrappers have the same false positive because their roots never dispatch evaluate_inverted_index() to the MATCH child. Require index execution to be enabled and a usable/index-evaluable root before selecting this strategy.

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.

在cloud 上可以认为 match 的执行一定有倒排索引的

expr->node_type() == TExprNodeType::SEARCH_EXPR || expr->can_push_down_to_index()) {
return true;
}
return std::ranges::any_of(expr->children(),
[](const auto& child) { return is_match_expr(child); });
}

static Status validate_residual_scan_conjuncts(RuntimeState* state,
TPushAggOp::type push_down_agg_type,
const VExprContextSPtrs& conjuncts) {
Expand Down Expand Up @@ -635,6 +646,21 @@ bool OlapScanLocalState::_is_binlog_merge_scan() const {
return scan_type == TBinlogScanType::MIN_DELTA || scan_type == TBinlogScanType::DETAIL;
}

// Give each segment a scanner of its own for queries like:
// SELECT k1 FROM t ORDER BY l2_distance_approximate(embedding, [1.0, 2.0]) LIMIT 2
// SELECT k1 FROM t WHERE msg MATCH_PHRASE 'error timeout'
bool OlapScanLocalState::_use_scan_parallelism_by_per_segment() {
// TODO: Use optimize_index_scan_parallelism for ann range search in the future.
// Currently, ann topn is enough
if (state()->query_options().__isset.optimize_index_scan_parallelism &&
state()->query_options().optimize_index_scan_parallelism && _ann_topn_runtime != nullptr) {
return true;
}
return config::is_cloud_mode() &&

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] Preserve global BM25 statistics before splitting by segment. This branch also covers score-enabled MATCH/SEARCH scans. Each generated scanner gets one segment from one rowset, and OlapScanner::_prepare_impl() builds a fresh CollectionStatistics from only that scanner's rs_splits; those rowset-local document-frequency and length values feed BM25. Rows from different rowsets are therefore ranked with incompatible IDF/avgdl values, so a final ORDER BY score() LIMIT ... can return the wrong row. Also, collection walks every segment of the supplied rowset, so an N-segment rowset now repeats that work N times. Compute/share statistics from the original complete read source, or keep score queries on the prior strategy, and add a cloud multi-rowset TopN regression.

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.

CollectionStatistics 一直是按scanner 的粒度构建的

std::ranges::any_of(_common_expr_ctxs_push_down,

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] Include pushed-down MATCH virtual-column projections in this decision. Projection-only MATCH is stored in _slot_id_to_virtual_column_expr, not _common_expr_ctxs_push_down, yet every scanner clones that context and SegmentIterator evaluates it over num_rows() for the whole segment before materializing the selected row range. A large segment split among N scanners therefore still repeats the same index evaluation N times, so this misses a supported path with the exact cost the PR targets. Inspect the virtual-column roots here as well (including any VirtualSlotRef unwrapping) and add a cloud projection regression.

@csun5285 csun5285 Sep 14, 2026

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.

不考虑 projection 先,仅针对 where 后面的倒排索引计算

[](const auto& ctx) { return is_match_expr(ctx->root()); });
}

Status OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
if (_scan_ranges.empty()) {
_eos = true;
Expand Down Expand Up @@ -764,15 +790,7 @@ Status OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
std::max<int64_t>(1024, state()->parallel_scan_min_rows_per_scanner());
scanner_builder.set_max_scanners_count(max_scanners_count);
scanner_builder.set_min_rows_per_scanner(min_rows_per_scanner);
// If the session variable is set, force one scanner per segment.
if (state()->query_options().__isset.optimize_index_scan_parallelism &&
state()->query_options().optimize_index_scan_parallelism) {
// TODO: Use optimize_index_scan_parallelism for ann range search in the future.
// Currently, ann topn is enough
if (_ann_topn_runtime != nullptr) {
scanner_builder.set_scan_parallelism_by_per_segment(true);
}
}
scanner_builder.set_scan_parallelism_by_per_segment(_use_scan_parallelism_by_per_segment());

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 segment-aligned scans within the configured scanner cap. _build_scanners_by_per_segment() creates one scanner for every segment and never consults parallel_scan_max_scanners_count or the minimum-rows setting; initialization then creates a delegate and pending task for every scanner before execution concurrency is applied. A cloud table with many historical segments can therefore allocate an unbounded scanner/task population even with the cap set to 1, and LIMIT/cancellation only clean it up afterward. Group whole segments into at most the configured number of scanner read sources so no segment is split while scanner cardinality remains bounded.

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.

scanner 变多的代价很低


RETURN_IF_ERROR(scanner_builder.build_scanners(*scanners));
for (auto& scanner : *scanners) {
Expand Down
3 changes: 3 additions & 0 deletions be/src/exec/operator/olap_scan_operator.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ class OlapScanLocalState final : public ScanLocalState<OlapScanLocalState> {

Status _init_scanners(std::list<ScannerSPtr>* scanners) override;

// Whether each segment should be scanned by a scanner of its own.
bool _use_scan_parallelism_by_per_segment();

Status _build_key_ranges_and_filters();

bool _is_tablet_pruned_by_runtime_filter(int64_t partition_id, int32_t bucket_seq,
Expand Down
Loading