Skip to content

Commit b99abe5

Browse files
authored
[feature](runtime filter) Support single-column runtime filter bucket pruning (#65837)
### What problem does this PR solve? Problem Summary: Runtime filters could prune table partitions but still created and scheduled scanners for every hash-distributed bucket. Add FE eligibility metadata for direct targets on a single HASH distribution column and use exact IN values in BE to compute Doris CRC bucket indexes. Initial and late filters skip nonmatching tablet scanners. Composite distribution and non-invertible Bloom filters conservatively fall back. ### Release note Support runtime-filter bucket pruning for exact filters on single-column HASH-distributed OLAP scans. It can be disabled with enable_runtime_filter_bucket_prune.
1 parent 3f330a7 commit b99abe5

40 files changed

Lines changed: 3049 additions & 671 deletions

be/src/exec/operator/olap_scan_operator.cpp

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ Status OlapScanLocalState::_init_profile() {
130130
_scan_rows = ADD_COUNTER(custom_profile(), "ScanRows", TUnit::UNIT);
131131
_tablets_pruned_by_rf_counter =
132132
ADD_COUNTER(custom_profile(), "TabletsPrunedByRuntimeFilter", TUnit::UNIT);
133+
_buckets_pruned_by_rf_counter =
134+
ADD_COUNTER(custom_profile(), "BucketsPrunedByRuntimeFilter", TUnit::UNIT);
133135

134136
// 1. init segment profile
135137
_segment_profile.reset(new RuntimeProfile("SegmentIterator"));
@@ -655,7 +657,7 @@ Status OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
655657
_cond_ranges.emplace_back(new doris::OlapScanRange());
656658
}
657659

658-
// Filter out tablets whose partitions have been pruned by runtime filters.
660+
// Filter out tablets whose partitions or buckets have been pruned by runtime filters.
659661
//
660662
// TODO(rf-partition-prune): this happens after OlapScanLocalState::init()
661663
// has already executed _sync_cloud_tablets() (in cloud mode that performs
@@ -670,13 +672,16 @@ Status OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
670672
// the tablet, (b) acquire ready-at-start RFs before _sync_cloud_tablets()
671673
// and run partition pruning there to filter _scan_ranges by partition_id
672674
// so the heavy per-tablet work is skipped for pruned partitions.
673-
if (_rf_partition_pruner.pruned_partition_count() > 0) {
675+
if (_rf_partition_pruner.pruned_partition_count() > 0 ||
676+
_rf_bucket_pruner.pruned_tablet_count() > 0) {
674677
DCHECK_EQ(_tablets.size(), _scan_ranges.size());
675678
DCHECK_EQ(_tablets.size(), _read_sources.size());
676679
size_t write_idx = 0;
677680
for (size_t read_idx = 0; read_idx < _tablets.size(); ++read_idx) {
678681
int64_t pid = _tablets[read_idx].tablet->partition_id();
679-
if (!_rf_partition_pruner.is_partition_pruned(pid)) {
682+
const auto& scan_range = *_scan_ranges[read_idx];
683+
if (!_is_tablet_pruned_by_runtime_filter(pid, scan_range.bucket_seq,
684+
scan_range.bucket_num)) {
680685
if (write_idx != read_idx) {
681686
_tablets[write_idx] = std::move(_tablets[read_idx]);
682687
_scan_ranges[write_idx] = std::move(_scan_ranges[read_idx]);
@@ -740,9 +745,9 @@ Status OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
740745
key_ranges.emplace_back(range.get());
741746
}
742747

743-
ParallelScannerBuilder scanner_builder(this, _tablets, _read_sources, _scanner_profile,
744-
key_ranges, state(), p._limit, true,
745-
p._olap_scan_node.is_preaggregation);
748+
ParallelScannerBuilder scanner_builder(this, _tablets, _read_sources, _scan_ranges,
749+
_scanner_profile, key_ranges, state(), p._limit,
750+
true, p._olap_scan_node.is_preaggregation);
746751

747752
int max_scanners_count = state()->parallel_scan_max_scanners_count();
748753

@@ -831,6 +836,8 @@ Status OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
831836
p._olap_scan_node.is_preaggregation,
832837
read_row_binlog,
833838
resolve_binlog_scan_type(palo_scan_range),
839+
palo_scan_range.bucket_seq,
840+
palo_scan_range.bucket_num,
834841
palo_scan_range.__isset.start_tso
835842
? std::make_optional(palo_scan_range.start_tso)
836843
: std::nullopt,
@@ -1111,13 +1118,54 @@ void OlapScanLocalState::set_scan_ranges(RuntimeState* state,
11111118
}
11121119
}
11131120

1114-
for (auto& scan_range : scan_ranges) {
1115-
DCHECK(scan_range.scan_range.__isset.palo_scan_range);
1121+
bool bucket_prune_metadata_initialized = !_scan_ranges.empty();
1122+
for (const auto& scan_range : scan_ranges) {
1123+
DORIS_CHECK(scan_range.scan_range.__isset.palo_scan_range);
11161124
_scan_ranges.emplace_back(new TPaloScanRange(scan_range.scan_range.palo_scan_range));
1125+
const auto& palo_scan_range = *_scan_ranges.back();
1126+
DORIS_CHECK_EQ(palo_scan_range.__isset.bucket_seq, palo_scan_range.__isset.bucket_num);
1127+
if (!bucket_prune_metadata_initialized) {
1128+
_has_rf_bucket_prune_metadata = palo_scan_range.__isset.bucket_seq;
1129+
bucket_prune_metadata_initialized = true;
1130+
} else {
1131+
DORIS_CHECK_EQ(palo_scan_range.__isset.bucket_seq, _has_rf_bucket_prune_metadata);
1132+
}
1133+
if (_has_rf_bucket_prune_metadata) {
1134+
DORIS_CHECK_GT(palo_scan_range.bucket_num, 0);
1135+
DORIS_CHECK_GE(palo_scan_range.bucket_seq, 0);
1136+
DORIS_CHECK_LT(palo_scan_range.bucket_seq, palo_scan_range.bucket_num);
1137+
}
11171138
COUNTER_UPDATE(_tablet_counter, 1);
11181139
}
11191140
}
11201141

1142+
Status OlapScanLocalState::_on_runtime_filter_update(const VExprContextSPtrs& new_conjuncts) {
1143+
RETURN_IF_ERROR(Base::_on_runtime_filter_update(new_conjuncts));
1144+
if (!state()->query_options().enable_runtime_filter_bucket_prune ||
1145+
!_has_rf_bucket_prune_metadata || _scan_ranges.empty()) {
1146+
return Status::OK();
1147+
}
1148+
1149+
int64_t newly_pruned = 0;
1150+
RETURN_IF_ERROR(_rf_bucket_pruner.prune_by_runtime_filters(
1151+
_scan_ranges, new_conjuncts, _parent->runtime_filter_descs(), _parent->node_id(),
1152+
state()->runtime_filter_max_in_num(), &newly_pruned));
1153+
if (newly_pruned > 0) {
1154+
COUNTER_SET(_buckets_pruned_by_rf_counter, _rf_bucket_pruner.pruned_tablet_count());
1155+
}
1156+
return Status::OK();
1157+
}
1158+
1159+
bool OlapScanLocalState::_is_tablet_pruned_by_runtime_filter(int64_t partition_id,
1160+
int32_t bucket_seq,
1161+
int32_t bucket_num) const {
1162+
if (_rf_partition_pruner.is_partition_pruned(partition_id)) {
1163+
return true;
1164+
}
1165+
return _has_rf_bucket_prune_metadata &&
1166+
_rf_bucket_pruner.is_bucket_pruned(bucket_seq, bucket_num);
1167+
}
1168+
11211169
static std::string tablets_id_to_string(
11221170
const std::vector<std::unique_ptr<TPaloScanRange>>& scan_ranges) {
11231171
if (scan_ranges.empty()) {

be/src/exec/operator/olap_scan_operator.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "common/status.h"
2828
#include "exec/operator/operator.h"
2929
#include "exec/operator/scan_operator.h"
30+
#include "exec/runtime_filter/runtime_filter_bucket_pruner.h"
3031
#include "runtime/runtime_profile.h"
3132
#include "storage/index/snii/snii_prx_profile.h"
3233
#include "storage/olap_scan_common.h"
@@ -78,6 +79,7 @@ class OlapScanLocalState final : public ScanLocalState<OlapScanLocalState> {
7879
const std::vector<TScanRangeParams>& scan_ranges) override;
7980
Status _init_profile() override;
8081
Status _process_conjuncts(RuntimeState* state) override;
82+
Status _on_runtime_filter_update(const VExprContextSPtrs& new_conjuncts) override;
8183
bool _is_key_column(const std::string& col_name) override;
8284

8385
bool can_push_down_column_predicate(const SlotDescriptor* slot) override;
@@ -135,7 +137,12 @@ class OlapScanLocalState final : public ScanLocalState<OlapScanLocalState> {
135137

136138
Status _build_key_ranges_and_filters();
137139

140+
bool _is_tablet_pruned_by_runtime_filter(int64_t partition_id, int32_t bucket_seq,
141+
int32_t bucket_num) const;
142+
138143
std::vector<std::unique_ptr<TPaloScanRange>> _scan_ranges;
144+
bool _has_rf_bucket_prune_metadata = false;
145+
RuntimeFilterBucketPruner _rf_bucket_pruner;
139146
std::vector<SyncRowsetStats> _sync_statistics;
140147
MonotonicStopWatch _sync_cloud_tablets_watcher;
141148
std::shared_ptr<Dependency> _cloud_tablet_dependency;
@@ -154,6 +161,7 @@ class OlapScanLocalState final : public ScanLocalState<OlapScanLocalState> {
154161
snii::SniiPhraseRuntimeProfileCounters _snii_phrase_profile_counters;
155162

156163
RuntimeProfile::Counter* _tablet_counter = nullptr;
164+
RuntimeProfile::Counter* _buckets_pruned_by_rf_counter = nullptr;
157165
RuntimeProfile::Counter* _key_range_counter = nullptr;
158166
RuntimeProfile::Counter* _reader_init_timer = nullptr;
159167
RuntimeProfile::Counter* _scanner_init_timer = nullptr;

be/src/exec/operator/scan_operator.cpp

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,25 +74,26 @@ bool ScanLocalState<Derived>::should_run_serial() const {
7474

7575
Status ScanLocalStateBase::update_late_arrival_runtime_filter(RuntimeState* state,
7676
int& arrived_rf_num) {
77-
// Lock needed because _conjuncts can be accessed concurrently by multiple scanner threads
77+
// Lock needed because _conjuncts can be accessed concurrently by multiple scanner threads.
7878
LockGuard lock(_conjuncts_lock);
7979
size_t conjuncts_before = _conjuncts.size();
8080
RETURN_IF_ERROR(_helper.try_append_late_arrival_runtime_filter(
8181
state, _parent->operator_row_desc_before_projection(), arrived_rf_num, _conjuncts));
82+
VExprContextSPtrs new_conjuncts;
83+
if (_conjuncts.size() > conjuncts_before) {
84+
new_conjuncts.assign(_conjuncts.begin() + conjuncts_before, _conjuncts.end());
85+
}
8286
if (state->enable_adjust_conjunct_order_by_cost()) {
8387
std::ranges::stable_sort(_conjuncts, [](const auto& a, const auto& b) {
8488
return a->execute_cost() < b->execute_cost();
8589
});
86-
};
87-
// Only re-run partition pruning when try_append_late_arrival_runtime_filter
88-
// actually appended new conjuncts. Otherwise this hook would re-scan all
89-
// partition boundaries on every scheduler pass while there are still
90-
// unapplied RFs (Scanner::_applied_rf_num is not advanced here), wasting
91-
// CPU re-evaluating the same set of RFs against the same boundaries.
92-
if (_conjuncts.size() > conjuncts_before) {
93-
RETURN_IF_ERROR(_on_runtime_filter_update());
9490
}
95-
return Status::OK();
91+
if (new_conjuncts.empty()) {
92+
return Status::OK();
93+
}
94+
// Partition projection executes the shared expression tree. Keep it serialized with
95+
// clone_conjunct_ctxs(), whose VExprContext::clone() opens that same tree.
96+
return _on_runtime_filter_update(new_conjuncts);
9697
}
9798

9899
Status ScanLocalStateBase::clone_conjunct_ctxs(VExprContextSPtrs& scanner_conjuncts) {
@@ -105,19 +106,15 @@ Status ScanLocalStateBase::clone_conjunct_ctxs(VExprContextSPtrs& scanner_conjun
105106
return Status::OK();
106107
}
107108

108-
bool ScanLocalStateBase::is_partition_pruned(int64_t partition_id) const {
109-
return _rf_partition_pruner.is_partition_pruned(partition_id);
110-
}
111-
112-
Status ScanLocalStateBase::_on_runtime_filter_update() {
109+
Status ScanLocalStateBase::_on_runtime_filter_update(const VExprContextSPtrs& new_conjuncts) {
113110
const auto* parsed = _parent->parsed_partition_boundaries();
114111
if (parsed != nullptr && !parsed->empty()) {
115-
RETURN_IF_ERROR(_do_partition_pruning_by_rf());
112+
RETURN_IF_ERROR(_do_partition_pruning_by_rf(new_conjuncts));
116113
}
117114
return Status::OK();
118115
}
119116

120-
Status ScanLocalStateBase::_do_partition_pruning_by_rf() {
117+
Status ScanLocalStateBase::_do_partition_pruning_by_rf(const VExprContextSPtrs& conjuncts) {
121118
if (!_state->query_options().enable_runtime_filter_partition_prune) {
122119
return Status::OK();
123120
}
@@ -127,7 +124,7 @@ Status ScanLocalStateBase::_do_partition_pruning_by_rf() {
127124
}
128125
int64_t newly_pruned = 0;
129126
RETURN_IF_ERROR(_rf_partition_pruner.prune_by_runtime_filters(
130-
*parsed, _conjuncts, _parent->runtime_filter_descs(), _parent->node_id(),
127+
*parsed, conjuncts, _parent->runtime_filter_descs(), _parent->node_id(),
131128
&newly_pruned));
132129
if (newly_pruned > 0) {
133130
COUNTER_SET(_partitions_pruned_by_rf_counter,
@@ -236,7 +233,8 @@ Status ScanLocalState<Derived>::open(RuntimeState* state) {
236233
RETURN_IF_ERROR(_helper.acquire_runtime_filter(state, _conjuncts,
237234
p.operator_row_desc_before_projection()));
238235
if (_conjuncts.size() > conjuncts_before) {
239-
RETURN_IF_ERROR(_on_runtime_filter_update());
236+
VExprContextSPtrs new_conjuncts(_conjuncts.begin() + conjuncts_before, _conjuncts.end());
237+
RETURN_IF_ERROR(_on_runtime_filter_update(new_conjuncts));
240238
}
241239

242240
// Disable condition cache in topn filter valid. TODO:: Try to support the topn filter in condition cache

be/src/exec/operator/scan_operator.h

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,6 @@ class ScanLocalStateBase : public PipelineXLocalState<> {
9898
[[nodiscard]] virtual int min_scanners_concurrency(RuntimeState* state) const;
9999
[[nodiscard]] virtual ScannerScheduler* scan_scheduler(RuntimeState* state) const;
100100

101-
// Thread-safe check whether a partition has been pruned by runtime filter.
102-
// Callable from any scan type's scanner in scheduling threads.
103-
bool is_partition_pruned(int64_t partition_id) const;
104-
105101
[[nodiscard]] std::string get_name() { return _parent->get_name(); }
106102

107103
uint64_t get_condition_cache_digest() const { return _condition_cache_digest; }
@@ -116,12 +112,12 @@ class ScanLocalStateBase : public PipelineXLocalState<> {
116112

117113
virtual Status _init_profile() = 0;
118114

119-
// Hook for subclasses to react after new runtime filters are appended.
120-
// Called inside update_late_arrival_runtime_filter() while _conjuncts_lock is held.
121-
// Default implementation runs partition pruning on the newly appended RFs.
122-
virtual Status _on_runtime_filter_update();
115+
// Hook for subclasses to process only the runtime-filter conjuncts appended by the current
116+
// update. Late-arrival updates call this while holding _conjuncts_lock because pruning may
117+
// execute expression nodes shared with scanner clones.
118+
virtual Status _on_runtime_filter_update(const VExprContextSPtrs& new_conjuncts);
123119

124-
Status _do_partition_pruning_by_rf();
120+
Status _do_partition_pruning_by_rf(const VExprContextSPtrs& conjuncts);
125121

126122
std::atomic<bool> _opened {false};
127123

@@ -296,10 +292,7 @@ class ScanLocalState : public ScanLocalStateBase {
296292
friend class Scanner;
297293

298294
Status _init_profile() override;
299-
virtual Status _process_conjuncts(RuntimeState* state) {
300-
RETURN_IF_ERROR(_do_partition_pruning_by_rf());
301-
return _normalize_conjuncts(state);
302-
}
295+
virtual Status _process_conjuncts(RuntimeState* state) { return _normalize_conjuncts(state); }
303296
virtual bool _should_push_down_common_expr(const VExprSPtr&) { return false; }
304297

305298
virtual bool can_push_down_column_predicate(const SlotDescriptor* slot) {

0 commit comments

Comments
 (0)