This is an automated email from the ASF dual-hosted git repository.

csun5285 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 211965dcb37 [opt](point query) Reuse segments for point query row data 
lookup (#67649)
211965dcb37 is described below

commit 211965dcb37bba6842a0f477046838d2432d8bd9
Author: zyp-V <[email protected]>
AuthorDate: Mon Oct 26 10:09:22 2026 +0800

    [opt](point query) Reuse segments for point query row data lookup (#67649)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary: Row-store point queries load rowset segments to locate
    a primary key, then load and search those segments again to read the row
    data. Return the matched segment from key lookup and retain it in the
    per-row read context so row-data lookup can use it directly. Keep the
    existing lookup interface for other callers and preserve the current
    branch's IOContext. This removes the second segment-loader call and
    segment scan from the row data stage; no latency or throughput benchmark
    was run.
    
    ### Release note
    
    Reduce repeated segment lookups in row-store point queries.
    
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [x] 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:
        - [x] 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 -->
    
    ---------
    
    Co-authored-by: zhangyipeng.0818 <[email protected]>
---
 be/src/service/point_query_executor.cpp        |  12 +-
 be/src/service/point_query_executor.h          |   2 +
 be/src/storage/segment/segment_loader.cpp      |   2 +
 be/src/storage/tablet/base_tablet.cpp          |  37 ++-
 be/src/storage/tablet/base_tablet.h            |  15 +-
 be/test/service/point_query_exector_test.cpp   | 351 +++++++++++++++++++++++++
 be/test/storage/segment/segment_cache_test.cpp | 103 +++++++-
 7 files changed, 509 insertions(+), 13 deletions(-)

diff --git a/be/src/service/point_query_executor.cpp 
b/be/src/service/point_query_executor.cpp
index 560fb3ca1e6..5fc85aebe8c 100644
--- a/be/src/service/point_query_executor.cpp
+++ b/be/src/service/point_query_executor.cpp
@@ -506,10 +506,10 @@ Status PointQueryExecutor::_lookup_row_key() {
         }
         // Get rowlocation and rowset, ctx._rowset_ptr will acquire wrap this 
ptr
         auto rowset_ptr = std::make_unique<RowsetSharedPtr>();
-        st = (_tablet->lookup_row_key(_row_read_ctxs[i]._primary_key, nullptr, 
false,
-                                      specified_rowsets, &location, INT32_MAX 
/*rethink?*/,
-                                      segment_caches, rowset_ptr.get(), false, 
nullptr,
-                                      &_profile_metrics.read_stats, nullptr, 
&io_ctx));
+        st = (_tablet->lookup_row_key(
+                _row_read_ctxs[i]._primary_key, nullptr, false, 
specified_rowsets, &location,
+                INT32_MAX /*rethink?*/, segment_caches, rowset_ptr.get(), 
false, nullptr,
+                &_profile_metrics.read_stats, nullptr, &io_ctx, 
&_row_read_ctxs[i]._segment));
         if (st.is<ErrorCode::KEY_NOT_FOUND>()) {
             continue;
         }
@@ -554,8 +554,8 @@ Status PointQueryExecutor::_lookup_row_data() {
                 io_ctx.remote_scan_cache_write_limiter = 
_remote_scan_cache_write_limiter.get();
                 RETURN_IF_ERROR(_tablet->lookup_row_data(
                         _row_read_ctxs[i]._primary_key, 
_row_read_ctxs[i]._row_location.value(),
-                        *(_row_read_ctxs[i]._rowset_ptr), 
_profile_metrics.read_stats, value,
-                        use_row_cache, &io_ctx));
+                        _row_read_ctxs[i]._segment, 
*(_row_read_ctxs[i]._rowset_ptr),
+                        _profile_metrics.read_stats, value, use_row_cache, 
&io_ctx));
                 // serialize value to block, currently only jsonb row format
                 RETURN_IF_ERROR(JsonbSerializeUtil::jsonb_to_columns(
                         _reusable->get_data_type_serdes(), value.data(), 
value.size(),
diff --git a/be/src/service/point_query_executor.h 
b/be/src/service/point_query_executor.h
index 91a8632b00a..285b77c6cd2 100644
--- a/be/src/service/point_query_executor.h
+++ b/be/src/service/point_query_executor.h
@@ -335,6 +335,8 @@ private:
         // rowset will be aquired during read
         // and released after used
         std::unique_ptr<RowsetSharedPtr, decltype(&release_rowset)> 
_rowset_ptr;
+        // Reuse the segment loaded during key lookup; release it before its 
acquired rowset.
+        segment_v2::SegmentSharedPtr _segment;
     };
 
     PTabletKeyLookupResponse* _response = nullptr;
diff --git a/be/src/storage/segment/segment_loader.cpp 
b/be/src/storage/segment/segment_loader.cpp
index c7f4a1a6d3b..8585267a420 100644
--- a/be/src/storage/segment/segment_loader.cpp
+++ b/be/src/storage/segment/segment_loader.cpp
@@ -21,6 +21,7 @@
 
 #include "common/config.h"
 #include "common/status.h"
+#include "cpp/sync_point.h"
 #include "storage/olap_define.h"
 #include "storage/rowset/beta_rowset.h"
 #include "storage/rowset/rowset_meta.h"
@@ -118,6 +119,7 @@ Status SegmentLoader::load_segments(const 
BetaRowsetSharedPtr& rowset,
                                     bool need_load_pk_index_and_bf,
                                     OlapReaderStatistics* index_load_stats,
                                     const io::IOContext* io_ctx) {
+    TEST_SYNC_POINT_CALLBACK("SegmentLoader::load_segments", rowset.get());
     if (cache_handle->is_inited()) {
         return Status::OK();
     }
diff --git a/be/src/storage/tablet/base_tablet.cpp 
b/be/src/storage/tablet/base_tablet.cpp
index eba76fe573b..ebb55462a1a 100644
--- a/be/src/storage/tablet/base_tablet.cpp
+++ b/be/src/storage/tablet/base_tablet.cpp
@@ -484,6 +484,26 @@ Status BaseTablet::lookup_row_data(const Slice& 
encoded_key, const RowLocation&
                                    RowsetSharedPtr input_rowset, 
OlapReaderStatistics& stats,
                                    std::string& values, bool write_to_cache,
                                    const io::IOContext* io_ctx) {
+    return _lookup_row_data(encoded_key, row_location, nullptr, 
std::move(input_rowset), stats,
+                            values, write_to_cache, io_ctx);
+}
+
+Status BaseTablet::lookup_row_data(const Slice& encoded_key, const 
RowLocation& row_location,
+                                   const segment_v2::SegmentSharedPtr& segment,
+                                   RowsetSharedPtr input_rowset, 
OlapReaderStatistics& stats,
+                                   std::string& values, bool write_to_cache,
+                                   const io::IOContext* io_ctx) {
+    DCHECK(segment != nullptr);
+    DCHECK_EQ(segment->id(), row_location.segment_id);
+    return _lookup_row_data(encoded_key, row_location, &segment, 
std::move(input_rowset), stats,
+                            values, write_to_cache, io_ctx);
+}
+
+Status BaseTablet::_lookup_row_data(const Slice& encoded_key, const 
RowLocation& row_location,
+                                    const segment_v2::SegmentSharedPtr* 
segment,
+                                    RowsetSharedPtr input_rowset, 
OlapReaderStatistics& stats,
+                                    std::string& values, bool write_to_cache,
+                                    const io::IOContext* io_ctx) const {
     MonotonicStopWatch watch;
     size_t row_size = 1;
     watch.start();
@@ -498,9 +518,14 @@ Status BaseTablet::lookup_row_data(const Slice& 
encoded_key, const RowLocation&
     SegmentCacheHandle segment_cache_handle;
     std::unique_ptr<segment_v2::ColumnIterator> column_iterator;
     const auto& column = 
*DORIS_TRY(tablet_schema->column(BeConsts::ROW_STORE_COL));
-    RETURN_IF_ERROR(_get_segment_column_iterator(rowset, 
row_location.segment_id, column,
-                                                 &segment_cache_handle, 
&column_iterator, stats,
-                                                 io_ctx));
+    if (segment != nullptr) {
+        RETURN_IF_ERROR(_init_segment_column_iterator(rowset, *segment, 
column, &column_iterator,
+                                                      stats, io_ctx));
+    } else {
+        RETURN_IF_ERROR(_get_segment_column_iterator(rowset, 
row_location.segment_id, column,
+                                                     &segment_cache_handle, 
&column_iterator, stats,
+                                                     io_ctx));
+    }
     // get and parse tuple row
     MutableColumnPtr column_ptr = ColumnString::create();
     std::vector<segment_v2::rowid_t> rowids 
{static_cast<segment_v2::rowid_t>(row_location.row_id)};
@@ -523,7 +548,8 @@ Status BaseTablet::lookup_row_key(const Slice& encoded_key, 
TabletSchema* latest
                                   
std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches,
                                   RowsetSharedPtr* rowset, bool with_rowid,
                                   std::string* encoded_seq_value, 
OlapReaderStatistics* stats,
-                                  DeleteBitmapPtr delete_bitmap, const 
io::IOContext* io_ctx) {
+                                  DeleteBitmapPtr delete_bitmap, const 
io::IOContext* io_ctx,
+                                  segment_v2::SegmentSharedPtr* segment) {
     SCOPED_BVAR_LATENCY(g_tablet_lookup_rowkey_latency);
     size_t seq_col_length = 0;
     // use the latest tablet schema to decide if the tablet has sequence 
column currently
@@ -608,6 +634,9 @@ Status BaseTablet::lookup_row_key(const Slice& encoded_key, 
TabletSchema* latest
             }
             TEST_SYNC_POINT_CALLBACK("BaseTablet::lookup_row_key:found", this, 
rs.get(),
                                      with_seq_col, s.code());
+            if (segment) {
+                *segment = segments[id];
+            }
             // find it and return
             return s;
         }
diff --git a/be/src/storage/tablet/base_tablet.h 
b/be/src/storage/tablet/base_tablet.h
index e570fc79944..c18247ea81f 100644
--- a/be/src/storage/tablet/base_tablet.h
+++ b/be/src/storage/tablet/base_tablet.h
@@ -182,6 +182,13 @@ public:
     Status lookup_row_data(const Slice& encoded_key, const RowLocation& 
row_location,
                            RowsetSharedPtr rowset, OlapReaderStatistics& 
stats, std::string& values,
                            bool write_to_cache = false, const io::IOContext* 
io_ctx = nullptr);
+
+    // Reuse the segment returned by lookup_row_key. The caller must keep the 
segment and
+    // its acquired rowset alive until this method returns.
+    Status lookup_row_data(const Slice& encoded_key, const RowLocation& 
row_location,
+                           const segment_v2::SegmentSharedPtr& segment, 
RowsetSharedPtr rowset,
+                           OlapReaderStatistics& stats, std::string& values,
+                           bool write_to_cache = false, const io::IOContext* 
io_ctx = nullptr);
     // Lookup the row location of `encoded_key`, the function sets 
`row_location` on success.
     // NOTE: the method only works in unique key model with primary key index, 
you will got a
     //       not supported error in other data model.
@@ -193,7 +200,8 @@ public:
                           std::string* encoded_seq_value = nullptr,
                           OlapReaderStatistics* stats = nullptr,
                           DeleteBitmapPtr tablet_delete_bitmap = nullptr,
-                          const io::IOContext* io_ctx = nullptr);
+                          const io::IOContext* io_ctx = nullptr,
+                          segment_v2::SegmentSharedPtr* segment = nullptr);
 
     // calc delete bitmap when flush memtable, use a fake version to calc
     // For example, cur max version is 5, and we use version 6 to calc but
@@ -371,6 +379,11 @@ public:
                                                                const 
CaptureRowsetOps& options);
 
 protected:
+    Status _lookup_row_data(const Slice& encoded_key, const RowLocation& 
row_location,
+                            const segment_v2::SegmentSharedPtr* segment, 
RowsetSharedPtr rowset,
+                            OlapReaderStatistics& stats, std::string& values, 
bool write_to_cache,
+                            const io::IOContext* io_ctx) const;
+
     // Protected by the tablet header lock; discovery resumes after this 
version.
     Version _changelog_ttl_scan_cursor {-1, -1};
 
diff --git a/be/test/service/point_query_exector_test.cpp 
b/be/test/service/point_query_exector_test.cpp
index 74bd7606e39..3e2269b2ae6 100644
--- a/be/test/service/point_query_exector_test.cpp
+++ b/be/test/service/point_query_exector_test.cpp
@@ -21,15 +21,31 @@
 #include <gen_cpp/internal_service.pb.h>
 #include <gtest/gtest.h>
 
+#include <atomic>
+
+#include "cloud/cloud_storage_engine.h"
+#include "cloud/cloud_tablet.h"
+#include "common/config.h"
 #include "common/object_pool.h"
 #include "core/block/block.h"
+#include "core/column/column_nullable.h"
+#include "core/field.h"
+#include "cpp/sync_point.h"
 #include "exprs/vexpr.h"
+#include "io/fs/local_file_system.h"
 #include "runtime/descriptor_helper.h"
 #include "runtime/descriptors.h"
 #include "runtime/exec_env.h"
+#include "runtime/memory/cache_manager.h"
 #include "runtime/runtime_state.h"
 #include "service/point_query_executor.h"
+#include "storage/options.h"
+#include "storage/rowset/beta_rowset.h"
+#include "storage/rowset/beta_rowset_writer.h"
+#include "storage/segment/segment_loader.h"
+#include "storage/storage_engine.h"
 #include "storage/tablet/tablet_schema.h"
+#include "util/thrift_util.h"
 
 namespace doris {
 
@@ -419,4 +435,339 @@ TEST_F(LookupConnectionCacheTest, PQTestEntryLifetime) {
     EXPECT_EQ(entry.use_count(), 2) << "Cache should maintain sole ownership 
after scope exit";
 }
 
+// Exercise the executor's key/data stages with real MoW segments. The 
parameters cover
+// local contiguous IDs and cloud segment-list IDs.
+class PointQuerySegmentReuseTest : public testing::TestWithParam<bool> {
+protected:
+    void SetUp() override {
+        _saved_disable_row_cache = config::disable_storage_row_cache;
+        _saved_disable_segment_cache = config::disable_segment_cache;
+        config::disable_storage_row_cache = true;
+        config::disable_segment_cache = false;
+        auto* env = ExecEnv::GetInstance();
+        _saved_cache_manager = env->get_cache_manager();
+        _cache_manager.reset(CacheManager::create_global_instance());
+        env->set_cache_manager(_cache_manager.get());
+        _saved_row_cache = env->get_row_cache();
+        _saved_segment_loader = env->segment_loader();
+        _row_cache.reset(new RowCache(1024 * 1024, 1));
+        _segment_loader = std::make_unique<SegmentLoader>(64 * 1024 * 1024, 
1024);
+        env->_row_cache = _row_cache.get();
+        env->set_segment_loader(_segment_loader.get());
+        EngineOptions options;
+        options.backend_uid = UniqueId::gen_uid();
+        _engine = std::make_unique<StorageEngine>(options);
+        
ASSERT_TRUE(io::global_local_filesystem()->delete_directory(kTestDir).ok());
+        
ASSERT_TRUE(io::global_local_filesystem()->create_directory(kTestDir).ok());
+    }
+
+    void TearDown() override {
+        auto* sync_point = SyncPoint::get_instance();
+        sync_point->clear_call_back("SegmentLoader::load_segments");
+        sync_point->disable_processing();
+        evict_segments();
+        _tablet.reset();
+        _rowsets.clear();
+        _cloud_engine.reset();
+        _engine.reset();
+        auto* env = ExecEnv::GetInstance();
+        env->_row_cache = _saved_row_cache;
+        env->set_segment_loader(_saved_segment_loader);
+        _row_cache.reset();
+        _segment_loader.reset();
+        env->set_cache_manager(_saved_cache_manager);
+        _cache_manager.reset();
+        config::disable_storage_row_cache = _saved_disable_row_cache;
+        config::disable_segment_cache = _saved_disable_segment_cache;
+        
EXPECT_TRUE(io::global_local_filesystem()->delete_directory(kTestDir).ok());
+    }
+
+    Status create_tablet() {
+        TabletMetaPB meta_pb;
+        meta_pb.set_tablet_id(987654);
+        meta_pb.set_enable_unique_key_merge_on_write(true);
+        meta_pb.set_tablet_state(PB_RUNNING);
+        auto* schema_pb = meta_pb.mutable_schema();
+        schema_pb->set_keys_type(UNIQUE_KEYS);
+        schema_pb->set_num_short_key_columns(1);
+        schema_pb->set_num_rows_per_row_block(1024);
+        schema_pb->set_next_column_unique_id(5);
+        schema_pb->set_delete_sign_idx(3);
+        schema_pb->set_store_row_column(true);
+        auto add_column = [&](int uid, const std::string& name, const 
std::string& type, int length,
+                              bool nullable) {
+            auto* column = schema_pb->add_column();
+            column->set_unique_id(uid);
+            column->set_name(name);
+            column->set_type(type);
+            column->set_length(length);
+            column->set_index_length(type == "STRING" ? 4 : length);
+            column->set_is_key(uid == 0);
+            column->set_is_nullable(nullable);
+            column->set_aggregation("NONE");
+        };
+        add_column(0, "k", "INT", 4, false);
+        add_column(1, "v1", "INT", 4, false);
+        add_column(2, "v2", "INT", 4, true);
+        add_column(3, DELETE_SIGN, "TINYINT", 1, false);
+        add_column(4, BeConsts::ROW_STORE_COL, "STRING", 2147483643, false);
+        auto meta = std::make_shared<TabletMeta>();
+        meta->init_from_pb(meta_pb);
+        if (GetParam()) {
+            _cloud_engine = std::make_unique<CloudStorageEngine>(EngineOptions 
{});
+            _tablet = std::make_shared<CloudTablet>(*_cloud_engine, meta);
+        } else {
+            _tablet = std::make_shared<Tablet>(*_engine, meta, nullptr);
+        }
+        RETURN_IF_ERROR(write_rowset(1, {{10, 11}, {20}, {30}}));
+        RETURN_IF_ERROR(write_rowset(2, {{40}}));
+        // Key 20 still has a primary-index entry but is invisible through the 
delete bitmap.
+        const auto& rowset = _rowsets.front();
+        meta->delete_bitmap().add({rowset->rowset_id(), 
rowset->segment(1).id(), 1}, 0);
+        auto* sync_point = SyncPoint::get_instance();
+        sync_point->set_call_back("SegmentLoader::load_segments", [&](auto&& 
args) {
+            if (try_any_cast<BetaRowset*>(args[0])->rowset_meta()->tablet_id() 
==
+                _tablet->tablet_id()) {
+                _segment_load_calls.fetch_add(1, std::memory_order_relaxed);
+            }
+        });
+        sync_point->enable_processing();
+        return Status::OK();
+    }
+
+    Status write_rowset(int64_t version, const 
std::vector<std::vector<int32_t>>& keys) {
+        RowsetWriterContext context;
+        context.rowset_id = _engine->next_rowset_id();
+        context.tablet_id = _tablet->tablet_id();
+        context.tablet_schema = _tablet->tablet_schema();
+        context.tablet_path = kTestDir;
+        context.rowset_state = VISIBLE;
+        context.version = {version, version};
+        context.enable_unique_key_merge_on_write = true;
+        context.enable_segcompaction = false;
+        context.write_type = DataWriteType::TYPE_DIRECT;
+        BetaRowsetWriter writer(*_engine);
+        RETURN_IF_ERROR(writer.init(context));
+        for (const auto& segment_keys : keys) {
+            Block block = context.tablet_schema->create_storage_block();
+            {
+                auto guard = block.mutate_columns_scoped();
+                auto& columns = guard.mutable_columns();
+                for (int32_t key : segment_keys) {
+                    columns[0]->insert(Field::create_field<TYPE_INT>(key));
+                    columns[1]->insert(Field::create_field<TYPE_INT>(key * 
10));
+                    if (key == 11) {
+                        columns[2]->insert_default();
+                    } else {
+                        columns[2]->insert(Field::create_field<TYPE_INT>(key * 
100));
+                    }
+                    columns[3]->insert_default();
+                    columns[4]->insert_default();
+                }
+            }
+            RETURN_IF_ERROR(writer.flush_single_block(&block));
+        }
+        RowsetSharedPtr rowset;
+        RETURN_IF_ERROR(writer.build(rowset));
+        if (GetParam()) {
+            // Model a cloud segment-list rowset using local files, without a 
remote service.
+            // Each rowset can use the same physical IDs; its rowset ID 
distinguishes the files.
+            std::vector<int64_t> segment_ids;
+            for (auto segment : rowset->segments()) {
+                auto path = DORIS_TRY(segment.path());
+                int64_t physical_id = 7 + segment.pos() * 13;
+                RETURN_IF_ERROR(io::global_local_filesystem()->rename(
+                        path, std::string(kTestDir) + "/" + 
rowset->rowset_id().to_string() + "_" +
+                                      std::to_string(physical_id) + ".dat"));
+                segment_ids.push_back(physical_id);
+            }
+            rowset->rowset_meta()->set_segment_ids(segment_ids);
+        }
+        {
+            std::unique_lock lock(_tablet->get_header_lock());
+            _tablet->_rs_version_map.emplace(rowset->version(), rowset);
+        }
+        _rowsets.push_back(std::move(rowset));
+        return Status::OK();
+    }
+
+    Status prepare_executor(PointQueryExecutor* executor, const 
std::vector<int32_t>& keys) {
+        TDescriptorTableBuilder descriptor_builder;
+        TTupleDescriptorBuilder tuple_builder;
+        for (int pos = 0; pos < 4; ++pos) {
+            const auto& column = _tablet->tablet_schema()->column(pos);
+            auto slot = TSlotDescriptorBuilder()
+                                .type(pos == 3 ? TYPE_TINYINT : TYPE_INT)
+                                .column_name(column.name())
+                                .column_pos(pos)
+                                .nullable(column.is_nullable())
+                                .build();
+            slot.__set_col_unique_id(column.unique_id());
+            tuple_builder.add_slot(slot);
+        }
+        tuple_builder.build(&descriptor_builder);
+        auto descriptor = descriptor_builder.desc_tbl();
+        std::vector<TExpr> output_exprs;
+        for (int pos = 0; pos < 3; ++pos) {
+            TExprNode node;
+            node.__set_node_type(TExprNodeType::SLOT_REF);
+            node.__set_type(descriptor.slotDescriptors[pos].slotType);
+            
node.__set_is_nullable(_tablet->tablet_schema()->column(pos).is_nullable());
+            node.__set_num_children(0);
+            TSlotRef slot_ref;
+            slot_ref.__set_slot_id(descriptor.slotDescriptors[pos].id);
+            slot_ref.__set_tuple_id(0);
+            node.__set_slot_ref(slot_ref);
+            TExpr expr;
+            expr.nodes.push_back(node);
+            output_exprs.push_back(expr);
+        }
+        TQueryOptions options;
+        executor->_tablet = _tablet;
+        executor->_reusable = std::make_shared<Reusable>();
+        RETURN_IF_ERROR(executor->_reusable->init(descriptor, output_exprs, 
options,
+                                                  *_tablet->tablet_schema(), 
1));
+        PTabletKeyLookupRequest request;
+        ThriftSerializer serializer(false, 128);
+        for (int32_t key : keys) {
+            TExprNode literal;
+            literal.__set_node_type(TExprNodeType::INT_LITERAL);
+            literal.__set_type(descriptor.slotDescriptors[0].slotType);
+            literal.__set_is_nullable(false);
+            literal.__set_num_children(0);
+            TIntLiteral int_literal;
+            int_literal.__set_value(key);
+            literal.__set_int_literal(int_literal);
+            RETURN_IF_ERROR(serializer.serialize(
+                    &literal, 
request.add_key_tuples()->add_key_column_literals()));
+        }
+        RETURN_IF_ERROR(executor->_init_keys(&request));
+        executor->_result_block = executor->_reusable->get_block();
+        return Status::OK();
+    }
+
+    void evict_segments() {
+        for (const auto& rowset : _rowsets) {
+            SegmentLoader::instance()->erase_segments(*rowset->rowset_meta());
+        }
+    }
+
+    static void check_rows(const Block& block) {
+        ASSERT_EQ(block.rows(), 4);
+        const std::vector<int32_t> expected_keys {30, 10, 40, 11};
+        const auto& v2 = assert_cast<const 
ColumnNullable&>(*block.get_by_position(2).column);
+        for (size_t row = 0; row < expected_keys.size(); ++row) {
+            EXPECT_EQ(block.get_by_position(0).column->get_int(row), 
expected_keys[row]);
+            EXPECT_EQ(block.get_by_position(1).column->get_int(row), 
expected_keys[row] * 10);
+            EXPECT_EQ(v2.is_null_at(row), expected_keys[row] == 11);
+            if (expected_keys[row] != 11) {
+                EXPECT_EQ(v2.get_nested_column().get_int(row), 
expected_keys[row] * 100);
+            }
+            EXPECT_EQ(block.get_by_position(3).column->get_int(row), 0);
+        }
+    }
+
+    static constexpr const char* kTestDir = "./point_query_segment_reuse_test";
+    std::unique_ptr<StorageEngine> _engine;
+    std::unique_ptr<CloudStorageEngine> _cloud_engine;
+    BaseTabletSPtr _tablet;
+    std::vector<RowsetSharedPtr> _rowsets;
+    std::atomic<int> _segment_load_calls = 0;
+    std::unique_ptr<RowCache> _row_cache;
+    std::unique_ptr<SegmentLoader> _segment_loader;
+    std::unique_ptr<CacheManager> _cache_manager;
+    CacheManager* _saved_cache_manager = nullptr;
+    RowCache* _saved_row_cache = nullptr;
+    SegmentLoader* _saved_segment_loader = nullptr;
+    bool _saved_disable_row_cache = true;
+    bool _saved_disable_segment_cache = false;
+};
+
+// NOLINTNEXTLINE(readability-function-cognitive-complexity): gtest assertions 
expand to branches; keep the key/data lifetime checks together.
+TEST_P(PointQuerySegmentReuseTest, ReuseAfterKeyLookupAndCacheEviction) {
+    ASSERT_EQ(Status::OK(), create_tablet());
+    std::vector<std::weak_ptr<segment_v2::Segment>> retained_segments;
+    {
+        PointQueryExecutor executor;
+        ASSERT_EQ(Status::OK(), prepare_executor(&executor, {30, 10, 40, 11, 
99, 20}));
+        ASSERT_EQ(Status::OK(), executor._lookup_row_key());
+        ASSERT_EQ(executor._row_hits, 4);
+        ASSERT_EQ(_segment_load_calls.load(), 2);
+        const auto& contexts = executor._row_read_ctxs;
+        for (size_t row = 0; row < 4; ++row) {
+            const auto& context = contexts[row];
+            ASSERT_TRUE(context._row_location.has_value());
+            ASSERT_NE(context._segment, nullptr);
+            ASSERT_NE(context._rowset_ptr, nullptr);
+            EXPECT_EQ(context._segment->id(), 
context._row_location->segment_id);
+            EXPECT_EQ((*context._rowset_ptr)->rowset_id(), 
context._row_location->rowset_id);
+            retained_segments.emplace_back(context._segment);
+        }
+        EXPECT_EQ(contexts[0]._segment->id(), _rowsets[0]->segment(2).id());
+        EXPECT_EQ(contexts[1]._segment.get(), contexts[3]._segment.get());
+        EXPECT_NE(contexts[1]._segment.get(), contexts[2]._segment.get());
+        EXPECT_EQ(contexts[3]._row_location->row_id, 1);
+        for (size_t row : {4, 5}) {
+            EXPECT_FALSE(contexts[row]._row_location.has_value());
+            EXPECT_EQ(contexts[row]._segment, nullptr);
+            EXPECT_EQ(contexts[row]._rowset_ptr, nullptr);
+        }
+        ASSERT_GT(_segment_loader->_segment_cache->get_element_count(), 0);
+        evict_segments();
+        ASSERT_EQ(_segment_loader->_segment_cache->get_element_count(), 0);
+        for (const auto& segment : retained_segments) {
+            EXPECT_FALSE(segment.expired());
+        }
+        ASSERT_EQ(Status::OK(), executor._lookup_row_data());
+        check_rows(*executor._result_block);
+        // This fails if the executor switches back to the overload that 
reloads segments.
+        EXPECT_EQ(_segment_load_calls.load(), 2);
+        EXPECT_EQ(_rowsets[0]->_refs_by_reader.load(), 3);
+        EXPECT_EQ(_rowsets[1]->_refs_by_reader.load(), 1);
+    }
+    for (const auto& segment : retained_segments) {
+        EXPECT_TRUE(segment.expired());
+    }
+    for (const auto& rowset : _rowsets) {
+        EXPECT_EQ(rowset->_refs_by_reader.load(), 0);
+    }
+}
+
+// NOLINTNEXTLINE(readability-function-cognitive-complexity): compare cold and 
cached execution using the same request and expected result.
+TEST_P(PointQuerySegmentReuseTest, LookupUpAndRowCacheHit) {
+    ASSERT_EQ(Status::OK(), create_tablet());
+    config::disable_storage_row_cache = false;
+    // MySQL text rows: length-prefixed cells, with 0xfb for SQL NULL.
+    const std::vector<std::string> expected_rows {"\00230\003300\0043000", 
"\00210\003100\0041000",
+                                                  "\00240\003400\0044000", 
"\00211\003110\373"};
+    for (bool cached : {false, true}) {
+        PointQueryExecutor executor;
+        PTabletKeyLookupResponse response;
+        // Only existing keys: the warm request must not need an index lookup 
for a missing key.
+        ASSERT_EQ(Status::OK(), prepare_executor(&executor, {30, 10, 40, 11}));
+        executor._response = &response;
+        _segment_load_calls = 0;
+        ASSERT_EQ(Status::OK(), executor.lookup_up());
+        EXPECT_FALSE(response.empty_batch());
+        TResultBatch batch;
+        auto length = static_cast<uint32_t>(response.row_batch().size());
+        ASSERT_TRUE(deserialize_thrift_msg(
+                            reinterpret_cast<const 
uint8_t*>(response.row_batch().data()), &length,
+                            false, &batch)
+                            .ok());
+        EXPECT_EQ(batch.rows, expected_rows);
+        EXPECT_EQ(_segment_load_calls.load(), cached ? 0 : 2);
+        EXPECT_EQ(executor._profile_metrics.row_cache_hits, cached ? 4 : 0);
+        if (cached) {
+            for (const auto& context : executor._row_read_ctxs) {
+                EXPECT_EQ(context._segment, nullptr);
+                EXPECT_EQ(context._rowset_ptr, nullptr);
+            }
+        }
+        evict_segments();
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(SegmentLayouts, PointQuerySegmentReuseTest, 
testing::Bool());
+
 } // namespace doris
diff --git a/be/test/storage/segment/segment_cache_test.cpp 
b/be/test/storage/segment/segment_cache_test.cpp
index 720aea1c0aa..19463fe2ecf 100644
--- a/be/test/storage/segment/segment_cache_test.cpp
+++ b/be/test/storage/segment/segment_cache_test.cpp
@@ -24,6 +24,7 @@
 #include <stdlib.h>
 #include <unistd.h>
 
+#include <atomic>
 #include <iostream>
 #include <map>
 #include <memory>
@@ -37,7 +38,9 @@
 #include "core/block/column_with_type_and_name.h"
 #include "core/column/column.h"
 #include "core/data_type/define_primitive_type.h"
+#include "core/field.h"
 #include "core/value/vdatetime_value.h"
+#include "cpp/sync_point.h"
 #include "exprs/function/cast/cast_to_date_or_datetime_impl.hpp"
 #include "gtest/gtest_pred_impl.h"
 #include "io/fs/local_file_system.h"
@@ -50,6 +53,7 @@
 #include "storage/iterators.h"
 #include "storage/olap_define.h"
 #include "storage/options.h"
+#include "storage/row_cursor.h"
 #include "storage/rowset/beta_rowset.h"
 #include "storage/schema.h"
 #include "storage/segment/segment.h"
@@ -60,6 +64,7 @@
 #include "storage/tablet_info.h"
 #include "storage/task/engine_publish_version_task.h"
 #include "storage/txn/txn_manager.h"
+#include "util/defer_op.h"
 
 namespace doris {
 class OlapMeta;
@@ -189,6 +194,12 @@ static TDescriptorTable 
create_descriptor_tablet_with_sequence_col() {
                                    .column_pos(4)
                                    .nullable(false)
                                    .build());
+    tuple_builder.add_slot(TSlotDescriptorBuilder()
+                                   .type(TYPE_STRING)
+                                   .column_name(BeConsts::ROW_STORE_COL)
+                                   .column_pos(5)
+                                   .nullable(false)
+                                   .build());
     tuple_builder.build(&dtb);
 
     return dtb.desc_tbl();
@@ -219,6 +230,7 @@ static void generate_data(Block* block, int8_t k1, int16_t 
k2, int32_t seq) {
 
     int32_t c5 = seq;
     columns[4]->insert_data((const char*)&c5, sizeof(c5));
+    columns[5]->insert_default();
     block->set_columns(std::move(columns));
 }
 
@@ -239,7 +251,17 @@ TEST_F(SegmentCacheTest, vec_sequence_col) {
     profile = std::make_unique<RuntimeProfile>("CreateTablet");
     TCreateTabletReq request;
     sleep(20);
-    create_tablet_request_with_sequence_col(55555, 270068377, &request);
+    create_tablet_request_with_sequence_col(55555, 270068377, &request, true);
+    request.tablet_schema.__set_store_row_column(true);
+    TColumn row_store_column;
+    row_store_column.column_name = BeConsts::ROW_STORE_COL;
+    row_store_column.column_type.type = TPrimitiveType::STRING;
+    row_store_column.__set_is_key(false);
+    row_store_column.__set_is_allow_null(false);
+    row_store_column.__set_aggregation_type(TAggregationType::REPLACE);
+    row_store_column.__set_default_value("");
+    row_store_column.__set_visible(false);
+    request.tablet_schema.columns.push_back(row_store_column);
     Status res = engine_ref->create_tablet(request, profile.get());
     ASSERT_TRUE(res.ok());
 
@@ -325,7 +347,6 @@ TEST_F(SegmentCacheTest, vec_sequence_col) {
     ASSERT_EQ(1, tablet->num_rows());
     std::vector<segment_v2::SegmentSharedPtr> segments;
 
-    SegmentCacheHandle handle;
     BetaRowsetSharedPtr rowset_ptr = 
std::dynamic_pointer_cast<BetaRowset>(rowset);
 
     std::mutex lock;
@@ -333,6 +354,7 @@ TEST_F(SegmentCacheTest, vec_sequence_col) {
         // Use lock to make sure only this segment can be loaded by 
SegmentLoader during this test.
         // SegmeentLoader is singleton. Without this lock, multiple segments 
will be loaded. Result will be wrong.
         std::lock_guard<std::mutex> l(lock);
+        SegmentCacheHandle handle;
         // load segments first
         int64_t start_size = SegmentLoader::instance()->cache_mem_usage();
         res = SegmentLoader::instance()->load_segments(rowset_ptr, &handle, 
true, true);
@@ -351,6 +373,83 @@ TEST_F(SegmentCacheTest, vec_sequence_col) {
                   segment_ptr->meta_mem_usage());
     }
 
+    std::atomic<int> segment_load_calls = 0;
+    auto* sync_point = SyncPoint::get_instance();
+    sync_point->set_call_back("SegmentLoader::load_segments", [&](auto&& args) 
{
+        if (try_any_cast<BetaRowset*>(args[0]) == rowset_ptr.get()) {
+            segment_load_calls.fetch_add(1, std::memory_order_relaxed);
+        }
+    });
+    sync_point->enable_processing();
+    Defer clear_sync_point([&]() {
+        sync_point->clear_call_back("SegmentLoader::load_segments");
+        sync_point->disable_processing();
+    });
+
+    RowCursor key_cursor;
+    ASSERT_TRUE(key_cursor
+                        .init_scan_key(tablet->tablet_schema(),
+                                       
{Field::create_field<TYPE_TINYINT>(int8_t {123}),
+                                        
Field::create_field<TYPE_SMALLINT>(int16_t {456})})
+                        .ok());
+    std::string encoded_key;
+    key_cursor.encode_key_with_padding<true>(&encoded_key,
+                                             
tablet->tablet_schema()->num_key_columns(), true);
+
+    std::vector<RowsetSharedPtr> specified_rowsets {rowset};
+    std::vector<std::unique_ptr<SegmentCacheHandle>> 
segment_caches(specified_rowsets.size());
+    RowLocation row_location;
+    RowsetSharedPtr found_rowset;
+    segment_v2::SegmentSharedPtr found_segment;
+    OlapReaderStatistics lookup_stats;
+    res = tablet->lookup_row_key(encoded_key, nullptr, false, 
specified_rowsets, &row_location,
+                                 version.second, segment_caches, 
&found_rowset, false, nullptr,
+                                 &lookup_stats, nullptr, nullptr, 
&found_segment);
+    ASSERT_TRUE(res.ok()) << res;
+    ASSERT_EQ(rowset.get(), found_rowset.get());
+    ASSERT_NE(nullptr, segment_caches[0]);
+    ASSERT_EQ(segment_caches[0]->get_segments().front().get(), 
found_segment.get());
+    ASSERT_EQ(found_segment->id(), row_location.segment_id);
+
+    const int load_calls_after_key_lookup = 
segment_load_calls.load(std::memory_order_relaxed);
+    ASSERT_EQ(1, load_calls_after_key_lookup);
+    std::string row_data;
+    res = tablet->lookup_row_data(encoded_key, row_location, found_segment, 
found_rowset,
+                                  lookup_stats, row_data, false);
+    ASSERT_TRUE(res.ok()) << res;
+    ASSERT_FALSE(row_data.empty());
+    EXPECT_EQ(load_calls_after_key_lookup, 
segment_load_calls.load(std::memory_order_relaxed));
+
+    std::string legacy_row_data;
+    res = tablet->lookup_row_data(encoded_key, row_location, found_rowset, 
lookup_stats,
+                                  legacy_row_data, false);
+    ASSERT_TRUE(res.ok()) << res;
+    EXPECT_EQ(row_data, legacy_row_data);
+    EXPECT_EQ(load_calls_after_key_lookup + 1, 
segment_load_calls.load(std::memory_order_relaxed));
+
+    // The executor drops its key-lookup cache handles before reading row 
data. Even if
+    // the cache entry is then evicted, the returned shared pointer must keep 
the segment alive.
+    segment_caches.clear();
+    SegmentLoader::instance()->erase_segments(*rowset->rowset_meta());
+    std::string uncached_row_data;
+    res = tablet->lookup_row_data(encoded_key, row_location, found_segment, 
found_rowset,
+                                  lookup_stats, uncached_row_data, false);
+    ASSERT_TRUE(res.ok()) << res;
+    EXPECT_EQ(row_data, uncached_row_data);
+    EXPECT_EQ(load_calls_after_key_lookup + 1, 
segment_load_calls.load(std::memory_order_relaxed));
+
+    // A deleted key must not return a segment for the row-data stage.
+    auto delete_bitmap = std::make_shared<DeleteBitmap>(tablet->tablet_id());
+    delete_bitmap->add({row_location.rowset_id, row_location.segment_id, 
version.second},
+                       row_location.row_id);
+    segment_caches.resize(specified_rowsets.size());
+    segment_v2::SegmentSharedPtr deleted_segment;
+    res = tablet->lookup_row_key(encoded_key, nullptr, false, 
specified_rowsets, &row_location,
+                                 version.second, segment_caches, nullptr, 
false, nullptr,
+                                 &lookup_stats, delete_bitmap, nullptr, 
&deleted_segment);
+    EXPECT_TRUE(res.is<ErrorCode::KEY_NOT_FOUND>()) << res;
+    EXPECT_EQ(nullptr, deleted_segment);
+
     res = ((BetaRowset*)rowset.get())->load_segments(&segments);
     ASSERT_TRUE(res.ok());
     ASSERT_EQ(1, rowset->num_segments());


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to