This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 32accf9301c [improvement](topn) Refresh TopN runtime filter page
ranges (#70020)
32accf9301c is described below
commit 32accf9301cb2db4f3197287f54971e2a45787dd
Author: TengJianPing <[email protected]>
AuthorDate: Fri Nov 6 09:55:52 2026 +0800
[improvement](topn) Refresh TopN runtime filter page ranges (#70020)
### What problem does this PR solve?
Issue Number: None
Problem Summary:
Segment readers keep row ranges computed before a TopN runtime bound
arrives or tightens. Scan-key normalization can also remove a shared
runtime predicate on a leading storage key. Readers then miss later
bound updates.
Publish versioned predicate snapshots and use page Zone Maps to refresh
unread row ranges between batches. Apply the first bound immediately and
throttle later refreshes by the number of raw rows read. Preserve scan
progress, bitmap holes, and read direction. Refresh the bitmap cursor,
cached cardinality, and filter counters together. Keep shared runtime
predicates during static scan-key normalization while still removing
static predicates covered by scan keys.
In the existing ascending TopN check with 1,048,576 rows and three sort
keys, ScanRows fell from 1,048,576 to 16,384 and TotalPagesNum fell from
372 to 6. These figures compare filtering disabled and enabled on the
same patched build. The query results were identical.
### Release note
TopN scans can skip unread pages when runtime bounds arrive or tighten.
Runtime predicates remain active after scan-key normalization.
### Check List (For Author)
- Test: Manual checks of commit boundaries, dependencies,
generated-result preservation, whitespace, clang-format v16, and header
hygiene. Include BE unit tests and two TopN regression suites. Builds
and tests were not rerun for this commit split.
- Behavior changed: Yes. TopN scans can read fewer rows and pages. This
commit retains inclusive runtime bounds.
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: close #xxx
Related PR: #xxx
Problem Summary:
### Release note
None
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [ ] Regression test
- [ ] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
- Behavior changed:
- [ ] No.
- [ ] Yes. <!-- Explain the behavior change -->
- Does this need documentation?
- [ ] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->
---
be/src/exec/operator/olap_scan_operator.cpp | 6 +
be/src/storage/predicate/shared_predicate.h | 23 +-
be/src/storage/segment/segment_iterator.cpp | 93 +++++-
be/src/storage/segment/segment_iterator.h | 15 +-
be/test/exec/operator/olap_scan_operator_test.cpp | 130 ++++++++
.../segment_iterator_topn_runtime_filter_test.cpp | 332 +++++++++++++++++++++
.../topn_runtime_predicate_key_range.out | 221 ++++++++++++++
.../topn_optimize/topn_runtime_range_update.out | 177 +++++++++++
.../topn_runtime_predicate_key_range.groovy | 46 +++
.../topn_optimize/topn_runtime_range_update.groovy | 39 +++
10 files changed, 1068 insertions(+), 14 deletions(-)
diff --git a/be/src/exec/operator/olap_scan_operator.cpp
b/be/src/exec/operator/olap_scan_operator.cpp
index 65c36930152..6197e3f4ec5 100644
--- a/be/src/exec/operator/olap_scan_operator.cpp
+++ b/be/src/exec/operator/olap_scan_operator.cpp
@@ -1417,6 +1417,12 @@ Status
OlapScanLocalState::_build_key_ranges_and_filters() {
// In either case, predicates with negation semantics
(effective NE / NOT_IN_LIST)
// are never subsumed by scan key ranges and must always be
preserved.
auto can_erase_predicate = [is_fixed_value_range](const
ColumnPredicate& pred) {
+ // Static scan keys do not capture runtime bounds. For
example, a full-range
+ // key cannot replace a TopN predicate that later tightens
to k1 <= 100.
+ // Keep the shared predicate installed so readers can
observe later updates.
+ if (pred.is_runtime_filter()) {
+ return false;
+ }
PredicateType pt = pred.type();
bool opposite = pred.opposite();
diff --git a/be/src/storage/predicate/shared_predicate.h
b/be/src/storage/predicate/shared_predicate.h
index 6d1a4aead08..48acb9a9cdc 100644
--- a/be/src/storage/predicate/shared_predicate.h
+++ b/be/src/storage/predicate/shared_predicate.h
@@ -17,6 +17,7 @@
#pragma once
+#include <atomic>
#include <cstdint>
#include <memory>
@@ -39,10 +40,6 @@ public:
: ColumnPredicate(column_id, col_name,
PrimitiveType::INVALID_TYPE),
_mtx(std::make_shared<std::shared_mutex>()) {}
SharedPredicate(const ColumnPredicate& other) = delete;
- SharedPredicate(const SharedPredicate& other, uint32_t column_id)
- : ColumnPredicate(other, column_id),
- _mtx(std::make_shared<std::shared_mutex>()),
- _nested(other._nested ? other._nested->clone(column_id) :
nullptr) {}
~SharedPredicate() override = default;
std::string debug_string() const override {
std::shared_lock<std::shared_mutex> lock(*_mtx);
@@ -56,6 +53,8 @@ public:
return std::const_pointer_cast<ColumnPredicate>(shared_from_this());
}
+ bool is_runtime_filter() const override { return true; }
+
PredicateType type() const override {
std::shared_lock<std::shared_mutex> lock(*_mtx);
if (!_nested) {
@@ -75,6 +74,21 @@ public:
void set_nested(const std::shared_ptr<ColumnPredicate>& nested) {
std::unique_lock<std::shared_mutex> lock(*_mtx);
_nested = nested;
+ _version.fetch_add(1, std::memory_order_release);
+ }
+
+ uint64_t version() const { return
_version.load(std::memory_order_acquire); }
+
+ struct Snapshot {
+ uint64_t version;
+ std::shared_ptr<const ColumnPredicate> predicate;
+ };
+
+ // Keep the immutable comparison alive after releasing the lock. A
concurrent update must
+ // not pair version 2 with version 1's bound, or hold the producer lock
during index I/O.
+ Snapshot snapshot() const {
+ std::shared_lock<std::shared_mutex> lock(*_mtx);
+ return {.version = _version.load(std::memory_order_relaxed),
.predicate = _nested};
}
Status evaluate(const IndexFieldNameAndTypePair& name_with_type,
IndexIterator* iterator,
@@ -191,6 +205,7 @@ private:
mutable std::shared_ptr<std::shared_mutex> _mtx;
std::shared_ptr<ColumnPredicate> _nested;
+ std::atomic<uint64_t> _version {0};
};
} //namespace doris
diff --git a/be/src/storage/segment/segment_iterator.cpp
b/be/src/storage/segment/segment_iterator.cpp
index 1d627b129f1..cb77918e8c8 100644
--- a/be/src/storage/segment/segment_iterator.cpp
+++ b/be/src/storage/segment/segment_iterator.cpp
@@ -98,6 +98,7 @@
#include "storage/olap_common.h"
#include "storage/predicate/bloom_filter_predicate.h"
#include "storage/predicate/column_predicate.h"
+#include "storage/predicate/shared_predicate.h"
#include "storage/schema.h"
#include "storage/segment/column_reader.h"
#include "storage/segment/column_reader_cache.h"
@@ -460,6 +461,11 @@ Status SegmentIterator::_init_impl(const
StorageReadOptions& opts) {
continue;
}
_col_predicates.emplace_back(predicate);
+ if (auto shared =
std::dynamic_pointer_cast<SharedPredicate>(predicate)) {
+ const auto version = shared->version();
+ _topn_runtime_filters.push_back(
+ {.predicate = std::move(shared), .applied_version =
version});
+ }
}
_init_column_states();
_tablet_id = opts.tablet_id;
@@ -617,15 +623,8 @@ Status SegmentIterator::_lazy_init(Block* block) {
RETURN_IF_ERROR(_apply_ann_topn_predicate());
- // _row_bitmap is immutable after _range_iter is created. Cache its
cardinality to avoid
- // traversing all roaring containers for every batch.
- _row_bitmap_cardinality = _row_bitmap.cardinality();
-
- if (_opts.read_orderby_key_reverse) {
- _range_iter.reset(new BackwardBitmapRangeIterator(_row_bitmap));
- } else {
- _range_iter.reset(new BitmapRangeIterator(_row_bitmap));
- }
+ _reset_range_iterator();
+ _topn_read_position = _opts.read_orderby_key_reverse ? num_rows() : 0;
// Reserve columns for _initial_block_row_max (the original max before any
adaptive
// prediction) because the predictor may increase block_row_max on
subsequent batches
@@ -1282,6 +1281,75 @@ Status SegmentIterator::_apply_ann_topn_predicate() {
return Status::OK();
}
+void SegmentIterator::_reset_range_iterator() {
+ // TopN pruning can replace the bitmap between batches. Refresh the cached
cardinality with
+ // the cursor so batch sizing does not use the larger range from before
the bound arrived.
+ _row_bitmap_cardinality = _row_bitmap.cardinality();
+ if (_opts.read_orderby_key_reverse) {
+ _range_iter.reset(new BackwardBitmapRangeIterator(_row_bitmap));
+ } else {
+ _range_iter.reset(new BitmapRangeIterator(_row_bitmap));
+ }
+}
+
+Status SegmentIterator::_update_topn_runtime_filter_ranges() {
+ // Apply the first bound immediately and throttle later changes by raw
rows read.
+ // A 40960-row delta is not enough; it must exceed the limit.
+ constexpr uint64_t update_threshold = 4096 * 10;
+ auto needs_update = [&](const TopNRuntimeFilter& filter) {
+ return filter.predicate->version() > filter.applied_version &&
+ (filter.applied_version == 0 ||
+ _topn_rows_read - filter.last_update_rows > update_threshold);
+ };
+ if (std::ranges::none_of(_topn_runtime_filters, needs_update)) {
+ return Status::OK();
+ }
+
+ SCOPED_RAW_TIMER(&_opts.stats->generate_row_ranges_by_zonemap_ns);
+ roaring::Roaring remaining = _row_bitmap;
+ // Exclude already consumed rows BEFORE intersecting page ranges. For
example, after reading
+ // [0,100), a surviving page [50,150) may only add [100,150) to the
rebuilt forward iterator.
+ if (_opts.read_orderby_key_reverse) {
+ remaining.removeRange(_topn_read_position, num_rows());
+ } else {
+ remaining.removeRange(0, _topn_read_position);
+ }
+ const auto before = remaining.cardinality();
+ // The last returned batch may have exhausted the cursor before the
producer publishes a
+ // tighter bound. There are no unread rows to test against page metadata
in that case.
+ if (before == 0) {
+ return Status::OK();
+ }
+ for (auto& filter : _topn_runtime_filters) {
+ if (!needs_update(filter)) {
+ continue;
+ }
+ const auto snapshot = filter.predicate->snapshot();
+ DCHECK(snapshot.predicate != nullptr);
+ AndBlockColumnPredicate predicate;
+ predicate.add_column_predicate(
+ SingleColumnBlockPredicate::create_unique(snapshot.predicate));
+ RowRanges ranges = RowRanges::create_single(num_rows());
+ RETURN_IF_ERROR(
+
_column_iterators[filter.predicate->column_id()]->get_row_ranges_by_zone_map(
+ &predicate, nullptr, &ranges));
+ remaining &= RowRanges::ranges_to_roaring(ranges);
+ filter.applied_version = snapshot.version;
+ filter.last_update_rows = _topn_rows_read;
+ }
+ const auto filtered = before - remaining.cardinality();
+ if (filtered != 0) {
+ // Roaring iterators borrow bitmap containers. Destroy the old cursor
before replacing
+ // its bitmap, then bind a new cursor to the remaining rows in the
same read direction.
+ _range_iter.reset();
+ _row_bitmap = std::move(remaining);
+ _reset_range_iterator();
+ _opts.stats->rows_stats_filtered += filtered;
+ _opts.stats->rows_stats_rp_filtered += filtered;
+ }
+ return Status::OK();
+}
+
Status SegmentIterator::_get_row_ranges_from_conditions(RowRanges*
condition_row_ranges) {
std::set<int32_t> cids;
for (auto& entry : _opts.col_id_to_predicates) {
@@ -2665,6 +2733,11 @@ Status SegmentIterator::_read_columns_by_index(const
std::vector<ColumnId>& read
SCOPED_RAW_TIMER(&_opts.stats->predicate_column_read_ns);
nrows_read =
(uint16_t)_range_iter->read_batch_rowids(_block_rowids.data(),
nrows_read_limit);
+ if (nrows_read > 0) {
+ _topn_read_position = _opts.read_orderby_key_reverse
+ ? _block_rowids[0]
+ : uint64_t(_block_rowids[nrows_read -
1]) + 1;
+ }
bool is_continuous = (nrows_read > 1) &&
(_block_rowids[nrows_read - 1] - _block_rowids[0] ==
nrows_read - 1);
VLOG_DEBUG << fmt::format(
@@ -3148,6 +3221,7 @@ Status SegmentIterator::_next_batch_internal(Block*
block) {
DCHECK(is_mem_reuse);
RETURN_IF_ERROR(_lazy_init(block));
+ RETURN_IF_ERROR(_update_topn_runtime_filter_ranges());
SCOPED_RAW_TIMER(&_opts.stats->block_load_ns);
@@ -3197,6 +3271,7 @@ Status SegmentIterator::_next_batch_internal(Block*
block) {
_opts.stats->blocks_load += 1;
_opts.stats->raw_rows_read += _selected_size;
+ _topn_rows_read += _selected_size;
if (_selected_size == 0) {
return _process_eof(block);
diff --git a/be/src/storage/segment/segment_iterator.h
b/be/src/storage/segment/segment_iterator.h
index 30fba1af4ba..0de8a0b6752 100644
--- a/be/src/storage/segment/segment_iterator.h
+++ b/be/src/storage/segment/segment_iterator.h
@@ -61,6 +61,7 @@ namespace doris {
class VExpr;
class VExprContext;
+class SharedPredicate;
namespace segment_v2 {
@@ -161,6 +162,9 @@ private:
std::vector<std::shared_ptr<ColumnPredicate>>&
remaining_predicates,
bool* continue_apply);
[[nodiscard]] Status _apply_ann_topn_predicate();
+
+ Status _update_topn_runtime_filter_ranges();
+ void _reset_range_iterator();
[[nodiscard]] Status _apply_index_expr();
// Publish _row_bitmap as IndexQueryContext::candidate_rows when it is
// below the configured engage ratio; refreshed at conjunct boundaries as
@@ -380,10 +384,19 @@ private:
std::vector<std::unique_ptr<IndexIterator>> _index_iterators;
// after init(), `_row_bitmap` contains all rowid to scan
roaring::Roaring _row_bitmap;
- // Cardinality cached after all row bitmap filters are applied in
_lazy_init().
+ // Cardinality cached when the range iterator is created or rebuilt after
TopN pruning.
uint64_t _row_bitmap_cardinality = 0;
// an iterator for `_row_bitmap` that can be used to extract row range to
scan
std::unique_ptr<BitmapRangeIterator> _range_iter;
+ struct TopNRuntimeFilter {
+ std::shared_ptr<SharedPredicate> predicate;
+ uint64_t applied_version;
+ uint64_t last_update_rows = 0;
+ };
+ std::vector<TopNRuntimeFilter> _topn_runtime_filters;
+ uint64_t _topn_rows_read = 0;
+ // Forward: first unread rowid. Reverse: exclusive upper bound of unread
rowids.
+ uint64_t _topn_read_position = 0;
// the next rowid to read
rowid_t _cur_rowid;
// members related to lazy materialization read
diff --git a/be/test/exec/operator/olap_scan_operator_test.cpp
b/be/test/exec/operator/olap_scan_operator_test.cpp
index 068d38f982c..5e7653107e0 100644
--- a/be/test/exec/operator/olap_scan_operator_test.cpp
+++ b/be/test/exec/operator/olap_scan_operator_test.cpp
@@ -19,17 +19,25 @@
#include <gtest/gtest.h>
+#include <array>
#include <memory>
#include <optional>
+#include <tuple>
#include "common/object_pool.h"
#include "core/column/column_const.h"
+#include "core/column/column_vector.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_string.h"
+#include "exec/pipeline/thrift_builder.h"
#include "exprs/vexpr_context.h"
+#include "exprs/vtopn_pred.h"
#include "gen_cpp/PlanNodes_types.h"
#include "gen_cpp/QueryCache_types.h"
+#include "runtime/query_context.h"
+#include "runtime/runtime_predicate.h"
+#include "storage/predicate/predicate_creator.h"
#include "storage/rowset/beta_rowset.h"
#include "testutil/column_helper.h"
#include "testutil/desc_tbl_builder.h"
@@ -360,4 +368,126 @@ TEST_F(OlapScanOperatorTsoPruningTest,
EmptyBootstrapDoesNotStartMinDeltaScanner
EXPECT_EQ(_local_state->_tablets_pruned_by_tso_counter->value(), 1);
}
+class OlapScanRuntimePredicateTest : public
testing::TestWithParam<std::tuple<bool, bool>> {
+protected:
+ void SetUp() override {
+ _state = std::make_shared<MockRuntimeState>();
+ DescriptorTblBuilder desc_builder(&_pool);
+ desc_builder.declare_tuple()
+ << TupleDescBuilder::SlotType
{std::make_shared<DataTypeInt32>(), "k1"};
+ _descs = desc_builder.build();
+ _slot = _descs->get_tuple_descriptor(0)->slots()[0];
+
+ TOlapScanNode scan_node;
+ scan_node.__set_tuple_id(0);
+ scan_node.__set_keyType(TKeysType::DUP_KEYS);
+ scan_node.__set_key_column_name({"k1"});
+ scan_node.__set_key_column_type({TPrimitiveType::INT});
+ TPlanNode plan_node;
+ plan_node.__set_node_id(0);
+ plan_node.__set_node_type(TPlanNodeType::OLAP_SCAN_NODE);
+ plan_node.__set_num_children(0);
+ plan_node.__set_row_tuples({0});
+ plan_node.__set_limit(-1);
+ plan_node.__set_olap_scan_node(scan_node);
+ _parent = std::make_shared<OlapScanOperatorX>(&_pool, plan_node, 0,
*_descs, 1,
+ TQueryCacheParam {});
+ _parent->_push_down_agg_type = TPushAggOp::NONE;
+ _parent->_colname_to_slot_id = {{"k1", _slot->id()}};
+ // The normalization capability check needs a physical column UID,
without opening tablets.
+ TColumn column;
+ column.__set_col_unique_id(0);
+ _parent->_olap_scan_node.__set_columns_desc({column});
+ _local_state = OlapScanLocalState::create_shared(_state.get(),
_parent.get());
+ _local_state->_custom_profile =
std::make_unique<RuntimeProfile>("RuntimePredicateTest");
+
+ TTopnFilterDesc desc;
+ desc.__set_source_node_id(10);
+ desc.__set_is_asc(std::get<0>(GetParam()));
+ desc.__set_null_first(false);
+ desc.__set_target_node_id_to_target_expr(
+ {{0, TRuntimeFilterDescBuilder::get_default_expr()}});
+ _state->get_query_ctx()->init_runtime_predicates({desc});
+ _runtime_predicate =
&_state->get_query_ctx()->get_runtime_predicate(10);
+ _runtime_predicate->set_detected_source();
+ ASSERT_TRUE(_runtime_predicate->init_target(0, {{_slot->id(), _slot}},
0).ok());
+ _shared_predicate = _runtime_predicate->get_predicate(0);
+ if (std::get<1>(GetParam())) {
+ ASSERT_TRUE(_runtime_predicate
+ ->update(Field::create_field<TYPE_INT>(
+ std::get<0>(GetParam()) ? 60 : 40))
+ .ok());
+ }
+ }
+
+ void check_retained(ColumnValueRange<TYPE_INT> range,
+ std::shared_ptr<ColumnPredicate> static_predicate =
nullptr) {
+ auto& predicates = _local_state->_slot_id_to_predicates[_slot->id()];
+ if (static_predicate) {
+ predicates.push_back(std::move(static_predicate));
+ }
+ _local_state->_slot_id_to_value_range[_slot->id()] = std::move(range);
+ VExprSPtr expr;
+
ASSERT_TRUE(VTopNPred::create_vtopn_pred(_runtime_predicate->get_texpr(0), 10,
expr).ok());
+ PushDownType push_down = PushDownType::UNACCEPTABLE;
+ ASSERT_TRUE(
+ _local_state->_normalize_topn_filter(nullptr, expr, _slot,
predicates, &push_down)
+ .ok());
+ ASSERT_EQ(push_down, PushDownType::ACCEPTABLE);
+ ASSERT_EQ(predicates.back(), _shared_predicate);
+
+ ASSERT_TRUE(_local_state->_build_key_ranges_and_filters().ok());
+ // Reaching an exact scan key must still remove its static range and
static predicate.
+ EXPECT_EQ(_local_state->_slot_id_to_value_range.count(_slot->id()), 0);
+ ASSERT_EQ(_local_state->_slot_id_to_predicates.count(_slot->id()), 1);
+ const auto& retained =
_local_state->_slot_id_to_predicates.at(_slot->id());
+ ASSERT_EQ(retained.size(), 1);
+ EXPECT_EQ(retained.front(), _shared_predicate);
+ EXPECT_TRUE(retained.front()->is_runtime_filter());
+ EXPECT_EQ(retained.front()->clone(0), _shared_predicate);
+
+ auto column = ColumnInt32::create();
+ column->get_data().assign({49, 50, 51});
+ std::array<bool, 3> matches {};
+ retained.front()->evaluate_vec(*column, matches.size(),
matches.data());
+ EXPECT_EQ(matches, (std::array<bool, 3> {true, true, true}));
+
ASSERT_TRUE(_runtime_predicate->update(Field::create_field<TYPE_INT>(50)).ok());
+ retained.front()->evaluate_vec(*column, matches.size(),
matches.data());
+ const bool ascending = std::get<0>(GetParam());
+ EXPECT_EQ(matches, (std::array<bool, 3> {ascending, true,
!ascending}));
+ }
+
+ ObjectPool _pool;
+ DescriptorTbl* _descs = nullptr;
+ SlotDescriptor* _slot = nullptr;
+ std::shared_ptr<MockRuntimeState> _state;
+ std::shared_ptr<OlapScanOperatorX> _parent;
+ std::shared_ptr<OlapScanLocalState> _local_state;
+ RuntimePredicate* _runtime_predicate = nullptr;
+ std::shared_ptr<ColumnPredicate> _shared_predicate;
+};
+
+TEST_P(OlapScanRuntimePredicateTest, WholeRangePreservesRuntimePredicate) {
+ check_retained(ColumnValueRange<TYPE_INT>("k1", false, 0, 0));
+}
+
+TEST_P(OlapScanRuntimePredicateTest, ScopeRangeRemovesOnlyStaticPredicate) {
+ ColumnValueRange<TYPE_INT> range("k1", false, 0, 0);
+ ASSERT_TRUE(range.add_range(SQLFilterOp::FILTER_LESS_OR_EQUAL, 100).ok());
+ check_retained(std::move(range),
+ create_comparison_predicate<PredicateType::LE>(
+ 0, "k1", _slot->type(),
Field::create_field<TYPE_INT>(100), false));
+}
+
+TEST_P(OlapScanRuntimePredicateTest, FixedRangeRemovesOnlyStaticPredicate) {
+ ColumnValueRange<TYPE_INT> range("k1", false, 0, 0);
+ ASSERT_TRUE(range.add_fixed_value(50).ok());
+ check_retained(std::move(range),
+ create_comparison_predicate<PredicateType::EQ>(
+ 0, "k1", _slot->type(),
Field::create_field<TYPE_INT>(50), false));
+}
+
+INSTANTIATE_TEST_SUITE_P(Bounds, OlapScanRuntimePredicateTest,
+ testing::Combine(testing::Bool(), testing::Bool()));
+
} // namespace doris
diff --git
a/be/test/storage/segment/segment_iterator_topn_runtime_filter_test.cpp
b/be/test/storage/segment/segment_iterator_topn_runtime_filter_test.cpp
new file mode 100644
index 00000000000..7ffd083b9d6
--- /dev/null
+++ b/be/test/storage/segment/segment_iterator_topn_runtime_filter_test.cpp
@@ -0,0 +1,332 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <memory>
+#include <set>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_number.h"
+#include "exec/pipeline/thrift_builder.h"
+#include "io/fs/file_writer.h"
+#include "io/fs/local_file_system.h"
+#include "runtime/descriptors.h"
+#include "runtime/runtime_predicate.h"
+#include "runtime/thread_context.h"
+#include "storage/index/zone_map/zone_map_index.h"
+#include "storage/predicate/block_column_predicate.h"
+#include "storage/row_cursor.h"
+#include "storage/segment/segment.h"
+#include "storage/segment/segment_iterator.h"
+#include "storage/segment/test_segment_writer.h"
+#include "storage/tablet/tablet_schema_helper.h"
+#include "testutil/mock/mock_runtime_state.h"
+
+namespace doris::segment_v2 {
+namespace {
+constexpr auto kTestDir = "./ut_dir/topn_runtime_ranges";
+constexpr int kRows = 131072;
+constexpr int kBound = kRows / 2 - 1;
+
+struct ScanResult {
+ std::vector<int32_t> ids;
+ int64_t raw_rows = 0;
+ int64_t pages = 0;
+ int64_t range_filtered = 0;
+};
+} // namespace
+
+class SegmentIteratorTopNRuntimeFilterTest : public testing::Test {
+protected:
+ void SetUp() override {
+ auto fs = io::global_local_filesystem();
+ ASSERT_TRUE(fs->delete_directory(kTestDir).ok());
+ ASSERT_TRUE(fs->create_directory(kTestDir).ok());
+ _tablet_schema = std::make_shared<TabletSchema>();
+ _tablet_schema->append_column(*create_int_key(0, false));
+ _tablet_schema->set_storage_page_size(4096);
+ _schema = std::make_shared<ReadSchema>(_tablet_schema->columns());
+ _slot._id = 0;
+ _slot._col_name = _tablet_schema->column(0).name();
+ _slot._type = std::make_shared<DataTypeInt32>();
+ ASSERT_NO_FATAL_FAILURE(write_segment());
+ }
+
+ void TearDown() override {
+ _reader.reset();
+ _segment.reset();
+
EXPECT_TRUE(io::global_local_filesystem()->delete_directory(kTestDir).ok());
+ }
+
+ void write_segment() {
+ const auto path = std::string(kTestDir) + "/segment_7.dat";
+ auto fs = io::global_local_filesystem();
+ io::FileWriterPtr file;
+ ASSERT_TRUE(fs->create_file(path, &file).ok());
+ VerticalSegmentWriterOptions options;
+ options.num_rows_per_block = 1024;
+ TestVerticalSegmentWriter writer(file.get(), 7, _tablet_schema,
nullptr, nullptr, options,
+ nullptr);
+ ASSERT_TRUE(writer.init().ok());
+ RowCursor row;
+ ASSERT_TRUE(row.init_scan_key(_tablet_schema,
{Field::create_field<TYPE_INT>(0)}).ok());
+ for (int rid = 0; rid < kRows; ++rid) {
+ row.mutable_field(0) = Field::create_field<TYPE_INT>(rid);
+ ASSERT_TRUE(writer.append_row(row).ok());
+ }
+ uint64_t index_size = 0;
+ uint64_t file_size = 0;
+ ASSERT_TRUE(writer.finalize_columns(&index_size).ok());
+ ASSERT_TRUE(writer.finalize_footer(&file_size).ok());
+ ASSERT_TRUE(file->close().ok());
+ ASSERT_TRUE(Segment::open(fs, path, 100, 7, RowsetId {.version = 1},
_tablet_schema,
+ io::FileReaderOptions {}, &_segment)
+ .ok());
+ }
+
+ void start(bool reverse = false, const Field& initial = Field(), bool
with_hole = false,
+ int batch_rows = 1024) {
+ _reader.reset();
+ _stats = OlapReaderStatistics {};
+ auto target = TRuntimeFilterDescBuilder::get_default_expr();
+ target.nodes[0].__set_type(create_type_desc(TYPE_INT));
+ TTopnFilterDesc desc;
+ desc.__set_source_node_id(10);
+ desc.__set_is_asc(!reverse);
+ desc.__set_null_first(false);
+ desc.__set_target_node_id_to_target_expr({{20, target}});
+ _predicate = std::make_unique<RuntimePredicate>(desc);
+ ASSERT_TRUE(_predicate->init_target(20, {{0, &_slot}}, 0).ok());
+ if (!initial.is_null()) {
+ ASSERT_TRUE(_predicate->update(initial).ok());
+ }
+ StorageReadOptions options(_stats);
+ options.runtime_state = &_state;
+ options.io_ctx.reader_type = ReaderType::READER_QUERY;
+ options.block_row_max = batch_rows;
+ options.read_orderby_key_reverse = reverse;
+ if (with_hole) {
+ options.row_ranges.add({0, 2048});
+ options.row_ranges.add({4096, kRows});
+ }
+ options.column_predicates = {_predicate->get_predicate(20)};
+ auto block_predicate = AndBlockColumnPredicate::create_shared();
+ block_predicate->add_column_predicate(
+
SingleColumnBlockPredicate::create_unique(options.column_predicates.front()));
+ options.col_id_to_predicates.emplace(0, std::move(block_predicate));
+ ASSERT_TRUE(_segment->new_iterator(_schema, options, &_reader).ok());
+ _block = _schema->create_read_block();
+ }
+
+ Status read(std::vector<int32_t>* ids) {
+ _block.clear_column_data();
+ RETURN_IF_ERROR(_reader->next_batch(&_block));
+ const auto& column = assert_cast<const
ColumnInt32&>(*_block.get_by_position(0).column);
+ ids->insert(ids->end(), column.get_data().begin(),
column.get_data().end());
+ return Status::OK();
+ }
+
+ void drain(std::vector<int32_t>* ids) {
+ while (true) {
+ const auto status = read(ids);
+ if (status.is<ErrorCode::END_OF_FILE>()) {
+ return;
+ }
+ ASSERT_TRUE(status.ok()) << status;
+ }
+ }
+
+ void late_scan(bool reverse, bool dynamic, ScanResult* result, int
batch_rows = 1024) {
+ ASSERT_NO_FATAL_FAILURE(start(reverse, Field(), false, batch_rows));
+ ASSERT_TRUE(read(&result->ids).ok());
+ ASSERT_EQ(batch_rows, result->ids.size());
+ auto* iterator = assert_cast<SegmentIterator*>(_reader.get());
+ if (!dynamic) {
+ // Preserve the old behavior as a control: current row predicates
still update.
+ iterator->_topn_runtime_filters.clear();
+ }
+ ASSERT_TRUE(_predicate->update(Field::create_field<TYPE_INT>(reverse ?
kBound + 1 : kBound))
+ .ok());
+ ASSERT_NO_FATAL_FAILURE(drain(&result->ids));
+ result->raw_rows = _stats.raw_rows_read;
+ result->pages = _stats.total_pages_num;
+ result->range_filtered = _stats.rows_stats_rp_filtered;
+ }
+
+ void expect_same_scan_result(const ScanResult& baseline, const ScanResult&
dynamic) {
+ EXPECT_EQ(dynamic.ids, baseline.ids);
+ EXPECT_EQ(kRows / 2, dynamic.ids.size());
+ EXPECT_EQ(kRows, baseline.raw_rows);
+ EXPECT_LT(dynamic.raw_rows, baseline.raw_rows);
+ EXPECT_LT(dynamic.pages, baseline.pages);
+ EXPECT_EQ(kRows - dynamic.raw_rows, dynamic.range_filtered);
+ const std::set<int32_t> unique(dynamic.ids.begin(), dynamic.ids.end());
+ EXPECT_EQ(unique.size(), dynamic.ids.size());
+ }
+
+ void check_late_scan(bool reverse) {
+ ScanResult baseline;
+ ScanResult dynamic;
+ ASSERT_NO_FATAL_FAILURE(late_scan(reverse, false, &baseline));
+ ASSERT_NO_FATAL_FAILURE(late_scan(reverse, true, &dynamic));
+ expect_same_scan_result(baseline, dynamic);
+ RecordProperty(reverse ? "reverse_raw_rows" : "forward_raw_rows",
+ std::to_string(dynamic.raw_rows));
+ RecordProperty(reverse ? "reverse_pages" : "forward_pages",
std::to_string(dynamic.pages));
+ RecordProperty("baseline_raw_rows", std::to_string(baseline.raw_rows));
+ RecordProperty("baseline_pages", std::to_string(baseline.pages));
+ }
+
+ TabletSchemaSPtr _tablet_schema;
+ ReadSchemaSPtr _schema;
+ std::shared_ptr<Segment> _segment;
+ std::unique_ptr<RowwiseIterator> _reader;
+ std::unique_ptr<RuntimePredicate> _predicate;
+ SlotDescriptor _slot;
+ OlapReaderStatistics _stats;
+ MockRuntimeState _state;
+ Block _block;
+};
+
+TEST_F(SegmentIteratorTopNRuntimeFilterTest,
LateArrivalSkipsPagesAndMatchesRowFiltering) {
+ ASSERT_NO_FATAL_FAILURE(check_late_scan(false));
+ ASSERT_NO_FATAL_FAILURE(check_late_scan(true));
+}
+
+TEST_F(SegmentIteratorTopNRuntimeFilterTest,
RebuildingRefreshesCachedBitmapCardinality) {
+ for (const bool reverse : {false, true}) {
+ SCOPED_TRACE(reverse);
+ ASSERT_NO_FATAL_FAILURE(start(reverse));
+ std::vector<int32_t> ids;
+ ASSERT_TRUE(read(&ids).ok());
+ const auto consumed = ids.size();
+ auto* iterator = assert_cast<SegmentIterator*>(_reader.get());
+ EXPECT_EQ(kRows, iterator->_row_bitmap_cardinality);
+
+ ASSERT_TRUE(_predicate->update(Field::create_field<TYPE_INT>(reverse ?
kBound + 1 : kBound))
+ .ok());
+ ASSERT_TRUE(iterator->_update_topn_runtime_filter_ranges().ok());
+ EXPECT_EQ(kRows / 2 - consumed, iterator->_row_bitmap_cardinality);
+ EXPECT_EQ(iterator->_row_bitmap.cardinality(),
iterator->_row_bitmap_cardinality);
+ ASSERT_TRUE(read(&ids).ok());
+ EXPECT_EQ(2 * consumed, ids.size());
+ }
+}
+
+TEST_F(SegmentIteratorTopNRuntimeFilterTest,
RebuildingInsideAPageKeepsOnlyItsUnreadRows) {
+ for (const bool reverse : {false, true}) {
+ ScanResult result;
+ ASSERT_NO_FATAL_FAILURE(late_scan(reverse, true, &result, 127));
+ const std::set<int32_t> unique(result.ids.begin(), result.ids.end());
+ EXPECT_EQ(kRows / 2, result.ids.size());
+ EXPECT_EQ(unique.size(), result.ids.size());
+ EXPECT_EQ(reverse ? kBound + 1 : 0, *unique.begin());
+ EXPECT_EQ(reverse ? kRows - 1 : kBound, *unique.rbegin());
+ }
+}
+
+TEST_F(SegmentIteratorTopNRuntimeFilterTest,
TighteningWaitsUntilRawRowThresholdIsExceeded) {
+ ASSERT_NO_FATAL_FAILURE(start(false,
Field::create_field<TYPE_INT>(kRows)));
+ std::vector<int32_t> ids;
+ for (int batch = 0; batch < 40; ++batch) {
+ ASSERT_TRUE(read(&ids).ok());
+ }
+ ASSERT_EQ(40960, _stats.raw_rows_read);
+
ASSERT_TRUE(_predicate->update(Field::create_field<TYPE_INT>(kBound)).ok());
+ ASSERT_TRUE(read(&ids).ok());
+ EXPECT_EQ(0, _stats.rows_stats_rp_filtered);
+ ASSERT_TRUE(read(&ids).ok());
+ EXPECT_GT(_stats.rows_stats_rp_filtered, 0);
+ ASSERT_NO_FATAL_FAILURE(drain(&ids));
+ EXPECT_EQ(kRows / 2, ids.size());
+ EXPECT_EQ(std::set<int32_t>(ids.begin(), ids.end()).size(), ids.size());
+}
+
+TEST_F(SegmentIteratorTopNRuntimeFilterTest,
UpdatesAtEofDoNotRewindOrCountConsumedRows) {
+ ScanResult result;
+ ASSERT_NO_FATAL_FAILURE(late_scan(false, true, &result));
+ const auto before = _stats.rows_stats_rp_filtered;
+ ASSERT_TRUE(_predicate->update(Field::create_field<TYPE_INT>(1)).ok());
+ auto* iterator = assert_cast<SegmentIterator*>(_reader.get());
+ ASSERT_TRUE(iterator->_update_topn_runtime_filter_ranges().ok());
+ EXPECT_EQ(before, _stats.rows_stats_rp_filtered);
+ EXPECT_EQ(kRows / 2, result.ids.size());
+}
+
+TEST_F(SegmentIteratorTopNRuntimeFilterTest,
RebuildingPreservesHolesFromInitialRestrictions) {
+ ASSERT_NO_FATAL_FAILURE(start(false, Field(), true));
+ std::vector<int32_t> ids;
+ ASSERT_TRUE(read(&ids).ok());
+ ASSERT_TRUE(_predicate->update(Field::create_field<TYPE_INT>(8191)).ok());
+ ASSERT_NO_FATAL_FAILURE(drain(&ids));
+ EXPECT_EQ(6144, ids.size());
+ EXPECT_EQ(std::set<int32_t>(ids.begin(), ids.end()).size(), ids.size());
+ EXPECT_TRUE(std::ranges::all_of(
+ ids, [](int id) { return (id < 2048 || id >= 4096) && id <= 8191;
}));
+}
+
+TEST_F(SegmentIteratorTopNRuntimeFilterTest,
SnapshotKeepsItsBoundAfterPublicationOfANewerVersion) {
+ ASSERT_NO_FATAL_FAILURE(start(false, Field::create_field<TYPE_INT>(100)));
+ auto shared =
std::dynamic_pointer_cast<SharedPredicate>(_predicate->get_predicate(20));
+ ASSERT_NE(nullptr, shared);
+ const auto snapshot = shared->snapshot();
+ ASSERT_EQ(1, snapshot.version);
+ ASSERT_TRUE(_predicate->update(Field::create_field<TYPE_INT>(100)).ok());
+ ASSERT_TRUE(_predicate->update(Field::create_field<TYPE_INT>(200)).ok());
+ EXPECT_EQ(1, shared->version());
+ ASSERT_TRUE(_predicate->update(Field::create_field<TYPE_INT>(50)).ok());
+ EXPECT_EQ(2, shared->version());
+ ZoneMap zone_map;
+ zone_map.has_not_null = true;
+ zone_map.min_value = Field::create_field<TYPE_INT>(75);
+ zone_map.max_value = zone_map.min_value;
+ EXPECT_TRUE(snapshot.predicate->evaluate_and(zone_map));
+ EXPECT_FALSE(shared->evaluate_and(zone_map));
+}
+
+TEST_F(SegmentIteratorTopNRuntimeFilterTest,
ConcurrentSnapshotsPairEachVersionWithItsBound) {
+ ASSERT_NO_FATAL_FAILURE(start(false,
Field::create_field<TYPE_INT>(kRows)));
+ auto shared =
std::dynamic_pointer_cast<SharedPredicate>(_predicate->get_predicate(20));
+ ASSERT_NE(nullptr, shared);
+ std::jthread writer([&] {
+ SCOPED_INIT_THREAD_CONTEXT();
+ for (int i = 1; i <= 1000; ++i) {
+ EXPECT_TRUE(_predicate->update(Field::create_field<TYPE_INT>(kRows
- i)).ok());
+ }
+ });
+ for (int i = 0; i < 1000; ++i) {
+ const auto snapshot = shared->snapshot();
+ const int boundary = kRows - static_cast<int>(snapshot.version - 1);
+ ZoneMap zone_map;
+ zone_map.has_not_null = true;
+ zone_map.min_value = Field::create_field<TYPE_INT>(boundary);
+ zone_map.max_value = zone_map.min_value;
+ EXPECT_TRUE(snapshot.predicate->evaluate_and(zone_map));
+ zone_map.min_value = Field::create_field<TYPE_INT>(boundary + 1);
+ zone_map.max_value = zone_map.min_value;
+ EXPECT_FALSE(snapshot.predicate->evaluate_and(zone_map));
+ }
+ writer.join();
+ EXPECT_EQ(1001, shared->version());
+}
+
+} // namespace doris::segment_v2
diff --git
a/regression-test/data/topn_optimize/topn_runtime_predicate_key_range.out
b/regression-test/data/topn_optimize/topn_runtime_predicate_key_range.out
new file mode 100644
index 00000000000..39b2dbb32f9
--- /dev/null
+++ b/regression-test/data/topn_optimize/topn_runtime_predicate_key_range.out
@@ -0,0 +1,221 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !asc_baseline --
+0 0 p081023
+0 0 p083199
+0 0 p085375
+0 0 p087551
+0 0 p089727
+0 0 p091903
+0 0 p094079
+0 0 p096255
+0 0 p098431
+0 0 p100607
+0 0 p102783
+0 0 p104959
+0 0 p107135
+0 0 p109311
+0 0 p111487
+0 0 p113663
+0 0 p115839
+0 0 p118015
+0 0 p120191
+0 0 p122367
+
+-- !desc_baseline --
+1008191 1 p000000
+1008190 14 p057345
+1008190 12 p049153
+1008190 10 p040961
+1008190 8 p032769
+1008190 6 p024577
+1008190 4 p016385
+1008190 2 p008193
+1008190 0 p000001
+1008189 16 p000002
+1008189 13 p057346
+1008189 11 p049154
+1008189 9 p040962
+1008189 7 p032770
+1008189 5 p024578
+1008189 3 p016386
+1008189 1 p008194
+1008188 15 p000003
+1008188 12 p057347
+1008188 10 p049155
+
+-- !null_baseline --
+\N 0 p000800
+\N 0 p002449
+\N 0 p004098
+\N 0 p005747
+\N 0 p007396
+\N 0 p009045
+\N 0 p010694
+\N 0 p012343
+\N 0 p013992
+\N 0 p015641
+\N 0 p017290
+\N 0 p018939
+\N 0 p020588
+\N 0 p022237
+\N 0 p023886
+\N 0 p025535
+\N 0 p027184
+\N 0 p028833
+\N 0 p030482
+\N 0 p032131
+
+-- !scope_baseline --
+0 0 p081023
+0 0 p083199
+0 0 p085375
+0 0 p087551
+0 0 p089727
+0 0 p091903
+0 0 p094079
+0 0 p096255
+0 0 p098431
+0 0 p100607
+0 0 p102783
+0 0 p104959
+0 0 p107135
+0 0 p109311
+0 0 p111487
+0 0 p113663
+0 0 p115839
+0 0 p118015
+0 0 p120191
+0 0 p122367
+
+-- !fixed_baseline --
+1 0 p081278
+1 0 p083454
+1 0 p085630
+1 0 p087806
+1 0 p089982
+1 0 p092158
+1 0 p094334
+1 0 p096510
+1 0 p098686
+1 0 p100862
+1 0 p105214
+1 0 p107390
+1 0 p109566
+1 0 p111742
+1 0 p113918
+1 0 p116094
+1 0 p118270
+1 0 p120446
+1 0 p122622
+1 0 p124798
+
+-- !asc_dynamic --
+0 0 p081023
+0 0 p083199
+0 0 p085375
+0 0 p087551
+0 0 p089727
+0 0 p091903
+0 0 p094079
+0 0 p096255
+0 0 p098431
+0 0 p100607
+0 0 p102783
+0 0 p104959
+0 0 p107135
+0 0 p109311
+0 0 p111487
+0 0 p113663
+0 0 p115839
+0 0 p118015
+0 0 p120191
+0 0 p122367
+
+-- !desc_dynamic --
+1008191 1 p000000
+1008190 14 p057345
+1008190 12 p049153
+1008190 10 p040961
+1008190 8 p032769
+1008190 6 p024577
+1008190 4 p016385
+1008190 2 p008193
+1008190 0 p000001
+1008189 16 p000002
+1008189 13 p057346
+1008189 11 p049154
+1008189 9 p040962
+1008189 7 p032770
+1008189 5 p024578
+1008189 3 p016386
+1008189 1 p008194
+1008188 15 p000003
+1008188 12 p057347
+1008188 10 p049155
+
+-- !null_dynamic --
+\N 0 p000800
+\N 0 p002449
+\N 0 p004098
+\N 0 p005747
+\N 0 p007396
+\N 0 p009045
+\N 0 p010694
+\N 0 p012343
+\N 0 p013992
+\N 0 p015641
+\N 0 p017290
+\N 0 p018939
+\N 0 p020588
+\N 0 p022237
+\N 0 p023886
+\N 0 p025535
+\N 0 p027184
+\N 0 p028833
+\N 0 p030482
+\N 0 p032131
+
+-- !scope_dynamic --
+0 0 p081023
+0 0 p083199
+0 0 p085375
+0 0 p087551
+0 0 p089727
+0 0 p091903
+0 0 p094079
+0 0 p096255
+0 0 p098431
+0 0 p100607
+0 0 p102783
+0 0 p104959
+0 0 p107135
+0 0 p109311
+0 0 p111487
+0 0 p113663
+0 0 p115839
+0 0 p118015
+0 0 p120191
+0 0 p122367
+
+-- !fixed_dynamic --
+1 0 p081278
+1 0 p083454
+1 0 p085630
+1 0 p087806
+1 0 p089982
+1 0 p092158
+1 0 p094334
+1 0 p096510
+1 0 p098686
+1 0 p100862
+1 0 p105214
+1 0 p107390
+1 0 p109566
+1 0 p111742
+1 0 p113918
+1 0 p116094
+1 0 p118270
+1 0 p120446
+1 0 p122622
+1 0 p124798
+
diff --git a/regression-test/data/topn_optimize/topn_runtime_range_update.out
b/regression-test/data/topn_optimize/topn_runtime_range_update.out
new file mode 100644
index 00000000000..881d0254cb7
--- /dev/null
+++ b/regression-test/data/topn_optimize/topn_runtime_range_update.out
@@ -0,0 +1,177 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !asc_baseline --
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+
+-- !desc_baseline --
+1008191
+1008190
+1008190
+1008190
+1008190
+1008190
+1008190
+1008190
+1008190
+1008189
+1008189
+1008189
+1008189
+1008189
+1008189
+1008189
+1008189
+1008188
+1008188
+1008188
+
+-- !null_baseline --
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+
+-- !ties_baseline --
+1008191 131071
+1008190 73726
+1008190 81918
+1008190 90110
+1008190 98302
+1008190 106494
+1008190 114686
+1008190 122878
+1008190 131070
+1008189 73725
+1008189 81917
+1008189 90109
+1008189 98301
+1008189 106493
+1008189 114685
+1008189 122877
+1008189 131069
+1008188 73724
+1008188 81916
+1008188 90108
+
+-- !asc_dynamic --
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+
+-- !desc_dynamic --
+1008191
+1008190
+1008190
+1008190
+1008190
+1008190
+1008190
+1008190
+1008190
+1008189
+1008189
+1008189
+1008189
+1008189
+1008189
+1008189
+1008189
+1008188
+1008188
+1008188
+
+-- !null_dynamic --
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+\N
+
+-- !ties_dynamic --
+1008191 131071
+1008190 73726
+1008190 81918
+1008190 90110
+1008190 98302
+1008190 106494
+1008190 114686
+1008190 122878
+1008190 131070
+1008189 73725
+1008189 81917
+1008189 90109
+1008189 98301
+1008189 106493
+1008189 114685
+1008189 122877
+1008189 131069
+1008188 73724
+1008188 81916
+1008188 90108
+
diff --git
a/regression-test/suites/topn_optimize/topn_runtime_predicate_key_range.groovy
b/regression-test/suites/topn_optimize/topn_runtime_predicate_key_range.groovy
new file mode 100644
index 00000000000..6fc92c2a006
--- /dev/null
+++
b/regression-test/suites/topn_optimize/topn_runtime_predicate_key_range.groovy
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("topn_runtime_predicate_key_range") {
+ sql "DROP TABLE IF EXISTS topn_runtime_predicate_key_range"
+ sql """CREATE TABLE topn_runtime_predicate_key_range (
+ k1 INT NULL, k2 INT NOT NULL, id BIGINT NOT NULL, payload VARCHAR(32)
NOT NULL
+ ) DUPLICATE KEY(k1,k2,id) DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ('replication_num'='1')"""
+ sql """INSERT INTO topn_runtime_predicate_key_range
+ SELECT CASE WHEN number % 97 = 0 THEN NULL
+ WHEN number < 65536 THEN CAST(number % 128 AS INT)
+ ELSE CAST(1000000 + number % 8192 AS INT) END,
+ CAST(number % 17 AS INT), number,
+ CONCAT('p', LPAD(CAST(131071 - number AS STRING), 6, '0'))
+ FROM numbers('number'='131072')"""
+
+ // The third ORDER BY column is not the third storage key. Read the whole
prefix tie group.
+ sql "SET topn_filter_ratio=0"
+ qt_asc_baseline "SELECT k1,k2,payload FROM
topn_runtime_predicate_key_range ORDER BY k1 NULLS LAST,k2,payload LIMIT 20
OFFSET 7"
+ qt_desc_baseline "SELECT k1,k2,payload FROM
topn_runtime_predicate_key_range ORDER BY k1 DESC NULLS LAST,k2 DESC,payload
DESC LIMIT 20 OFFSET 7"
+ qt_null_baseline "SELECT k1,k2,payload FROM
topn_runtime_predicate_key_range ORDER BY k1 NULLS FIRST,k2,payload LIMIT 20"
+ qt_scope_baseline "SELECT k1,k2,payload FROM
topn_runtime_predicate_key_range WHERE k1 >= 0 AND k1 < 64 ORDER BY
k1,k2,payload LIMIT 20 OFFSET 7"
+ qt_fixed_baseline "SELECT k1,k2,payload FROM
topn_runtime_predicate_key_range WHERE k1 IN (1,2) ORDER BY k1,k2,payload LIMIT
20 OFFSET 7"
+
+ sql "SET topn_filter_ratio=1000000"
+ qt_asc_dynamic "SELECT k1,k2,payload FROM topn_runtime_predicate_key_range
ORDER BY k1 NULLS LAST,k2,payload LIMIT 20 OFFSET 7"
+ qt_desc_dynamic "SELECT k1,k2,payload FROM
topn_runtime_predicate_key_range ORDER BY k1 DESC NULLS LAST,k2 DESC,payload
DESC LIMIT 20 OFFSET 7"
+ qt_null_dynamic "SELECT k1,k2,payload FROM
topn_runtime_predicate_key_range ORDER BY k1 NULLS FIRST,k2,payload LIMIT 20"
+ qt_scope_dynamic "SELECT k1,k2,payload FROM
topn_runtime_predicate_key_range WHERE k1 >= 0 AND k1 < 64 ORDER BY
k1,k2,payload LIMIT 20 OFFSET 7"
+ qt_fixed_dynamic "SELECT k1,k2,payload FROM
topn_runtime_predicate_key_range WHERE k1 IN (1,2) ORDER BY k1,k2,payload LIMIT
20 OFFSET 7"
+}
diff --git
a/regression-test/suites/topn_optimize/topn_runtime_range_update.groovy
b/regression-test/suites/topn_optimize/topn_runtime_range_update.groovy
new file mode 100644
index 00000000000..0fe2f9b83c8
--- /dev/null
+++ b/regression-test/suites/topn_optimize/topn_runtime_range_update.groovy
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("topn_runtime_range_update") {
+ sql "DROP TABLE IF EXISTS topn_runtime_range_update"
+ sql """CREATE TABLE topn_runtime_range_update (
+ id BIGINT NOT NULL, k INT NULL
+ ) DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ('replication_num'='1')"""
+ sql """INSERT INTO topn_runtime_range_update
+ SELECT number, CASE WHEN number % 97 = 0 THEN NULL
+ WHEN number < 65536 THEN CAST(number AS INT)
+ ELSE CAST(1000000 + number % 8192 AS INT) END
+ FROM numbers('number'='131072')"""
+ sql "SET topn_filter_ratio=0"
+ qt_asc_baseline "SELECT k FROM topn_runtime_range_update ORDER BY k NULLS
LAST LIMIT 20 OFFSET 7"
+ qt_desc_baseline "SELECT k FROM topn_runtime_range_update ORDER BY k DESC
NULLS LAST LIMIT 20 OFFSET 7"
+ qt_null_baseline "SELECT k FROM topn_runtime_range_update ORDER BY k NULLS
FIRST LIMIT 20"
+ qt_ties_baseline "SELECT k,id FROM topn_runtime_range_update ORDER BY k
DESC NULLS LAST,id LIMIT 20 OFFSET 7"
+ sql "SET topn_filter_ratio=1000000"
+ qt_asc_dynamic "SELECT k FROM topn_runtime_range_update ORDER BY k NULLS
LAST LIMIT 20 OFFSET 7"
+ qt_desc_dynamic "SELECT k FROM topn_runtime_range_update ORDER BY k DESC
NULLS LAST LIMIT 20 OFFSET 7"
+ qt_null_dynamic "SELECT k FROM topn_runtime_range_update ORDER BY k NULLS
FIRST LIMIT 20"
+ qt_ties_dynamic "SELECT k,id FROM topn_runtime_range_update ORDER BY k
DESC NULLS LAST,id LIMIT 20 OFFSET 7"
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]