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

gavinchou 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 e925e6ae1b2 [feature](binlog) Add per-row LSN column for detail (DUP) 
tables to  support IVM (#66889)
e925e6ae1b2 is described below

commit e925e6ae1b2abe5d8d2c2ef1e1677ec14d014d58
Author: Userwhite <[email protected]>
AuthorDate: Thu Aug 27 12:07:42 2026 +0800

    [feature](binlog) Add per-row LSN column for detail (DUP) tables to  
support IVM (#66889)
    
    Problem Summary:
    
    Incremental View Maintenance (IVM) needs a stable per-row identity for
    every row in the base table. For MOW/unique tables this can be derived
    from the unique key, but a detail (DUP_KEYS) table has no stable unique
    key — two rows can be byte-identical, and the commit TSO alone is not
    row-unique within one commit.
    
    Row Binlog already allocates a monotonic LSN per row so that binlog rows
    can be ordered/deduplicated. This PR surfaces that same LSN as a
    first-class hidden column on the DUP base table, and makes Stream return
    the LSN alongside the existing sequence (TSO) column, so IVM has a
    stable (TSO, LSN) identity for every base row.
    
    Scope (intentional):
    - Only the DUP base index with row-binlog enabled gets the base
    __DORIS_ROW_LSN_COL__ column.
    - MOW does not expose or store a base LSN column (a MOW row already has
    a stable unique key; adding an aggregated per-row LSN there is
    meaningless and would complicate merge semantics). MOW still uses the
    LSN internally for the row-binlog side-channel.
    - Rollup / MV indexes are not covered; only the base index stores the
    row LSN.
    
    Design overview:
    
    <img width="2860" height="2480" alt="image"
    
src="https://github.com/user-attachments/assets/889af47f-a495-441c-8d47-29eb23fcb0e9";
    />
---
 be/src/cloud/cloud_rowset_builder.cpp              |  1 +
 be/src/cloud/cloud_rowset_writer.cpp               |  4 +
 be/src/cloud/pb_convert.cpp                        | 12 +++
 be/src/exec/sink/writer/vtablet_writer.cpp         | 22 +++---
 be/src/exec/sink/writer/vtablet_writer.h           |  6 +-
 be/src/load/channel/tablets_channel.cpp            | 28 +++----
 be/src/load/delta_writer/delta_writer_context.h    |  2 +-
 be/src/load/memtable/memtable.cpp                  | 86 ++++++++++++----------
 be/src/load/memtable/memtable.h                    | 25 ++++---
 be/src/load/memtable/memtable_flush_executor.cpp   | 25 ++++---
 be/src/load/memtable/memtable_flush_executor.h     |  3 +
 be/src/load/memtable/memtable_writer.cpp           | 23 +++---
 be/src/load/memtable/memtable_writer.h             |  2 +-
 be/src/storage/binlog.h                            | 45 ++++-------
 be/src/storage/rowset/beta_rowset_writer.cpp       |  3 +
 be/src/storage/rowset/beta_rowset_writer_v2.cpp    |  4 +
 be/src/storage/rowset/group_rowset_writer.cpp      | 18 +++++
 be/src/storage/rowset/group_rowset_writer.h        |  5 +-
 be/src/storage/rowset/rowset_fwd.h                 |  3 +
 be/src/storage/rowset/rowset_writer_context.h      | 26 +++++++
 be/src/storage/rowset_builder.cpp                  |  1 +
 be/src/storage/tablet/base_tablet.cpp              |  4 +-
 be/src/storage/tablet/tablet_meta.cpp              |  6 ++
 be/src/storage/tablet/tablet_schema.cpp            |  9 +++
 be/src/storage/tablet/tablet_schema.h              |  2 +
 be/src/storage/transform/row_binlog_derive.cpp     |  4 +-
 be/src/storage/transform/row_binlog_derive.h       |  3 +-
 be/src/storage/utils.h                             |  1 +
 .../load/memtable/memtable_flush_executor_test.cpp | 18 ++++-
 .../cloud_group_rowset_builder_writer_test.cpp     | 11 ++-
 be/test/olap/rowset/group_rowset_writer_test.cpp   | 22 ++++--
 be/test/storage/mow/mow_transform_test_base.h      |  3 +-
 .../storage/rowset/segment_flusher_format_test.cpp |  3 +-
 .../storage/transform/row_binlog_derive_test.cpp   | 41 ++++++-----
 .../main/java/org/apache/doris/catalog/Column.java |  8 ++
 .../java/org/apache/doris/catalog/OlapTable.java   |  5 ++
 .../catalog/stream/TableStreamBuildFactory.java    |  8 ++
 .../cloud/datasource/CloudInternalCatalog.java     |  5 ++
 .../apache/doris/datasource/InternalCatalog.java   |  2 +-
 .../rewrite/NormalizeOlapTableStreamScan.java      | 71 +++++++++---------
 .../plans/commands/info/ColumnDefinition.java      | 12 +++
 .../trees/plans/commands/info/CreateTableInfo.java | 19 ++---
 .../plans/logical/LogicalOlapTableStreamScan.java  | 26 +++++--
 .../org/apache/doris/task/CreateReplicaTask.java   |  5 ++
 gensrc/proto/internal_service.proto                |  5 +-
 gensrc/proto/olap_file.proto                       | 18 +++--
 gensrc/thrift/AgentService.thrift                  |  7 +-
 .../row_binlog_p0/test_row_binlog_basic.groovy     | 27 +++++++
 .../test_olap_table_stream_history_query.groovy    | 25 +++++--
 .../test_olap_table_stream_snapshot.groovy         |  8 ++
 .../test_table_stream_query_comprehensive.groovy   | 17 +++++
 51 files changed, 495 insertions(+), 244 deletions(-)

diff --git a/be/src/cloud/cloud_rowset_builder.cpp 
b/be/src/cloud/cloud_rowset_builder.cpp
index 0ab26eef6a5..8730b86a555 100644
--- a/be/src/cloud/cloud_rowset_builder.cpp
+++ b/be/src/cloud/cloud_rowset_builder.cpp
@@ -139,6 +139,7 @@ Status CloudGroupRowsetBuilder::init() {
     
RETURN_IF_ERROR(RowsetFactory::create_empty_group_rowset_writer(&group_writer));
     group_writer->set_data_writer(_data_builder->rowset_writer());
     group_writer->set_row_binlog_writer(_row_binlog_builder->rowset_writer());
+    
RETURN_IF_ERROR(group_writer->init(_data_builder->rowset_writer()->context()));
 
     {
         const auto& data_ctx = _data_builder->rowset_writer()->context();
diff --git a/be/src/cloud/cloud_rowset_writer.cpp 
b/be/src/cloud/cloud_rowset_writer.cpp
index d214a9ff0b4..a0ef7977c59 100644
--- a/be/src/cloud/cloud_rowset_writer.cpp
+++ b/be/src/cloud/cloud_rowset_writer.cpp
@@ -41,6 +41,10 @@ CloudRowsetWriter::~CloudRowsetWriter() {
 
 Status CloudRowsetWriter::init(const RowsetWriterContext& 
rowset_writer_context) {
     _context = rowset_writer_context;
+    // Row-binlog writer or a schema carrying ROW_LSN_COL needs allocated LSN.
+    _context._need_allocate_lsn =
+            _context.write_binlog_opt().enable ||
+            (_context.tablet_schema != nullptr && 
_context.tablet_schema->row_lsn_col_idx() >= 0);
     _rowset_meta = std::make_shared<RowsetMeta>();
 
     if (_context.is_local_rowset()) {
diff --git a/be/src/cloud/pb_convert.cpp b/be/src/cloud/pb_convert.cpp
index 900aca4c7fc..b7a48e0dbb6 100644
--- a/be/src/cloud/pb_convert.cpp
+++ b/be/src/cloud/pb_convert.cpp
@@ -504,6 +504,9 @@ void doris_tablet_schema_to_cloud(TabletSchemaCloudPB* out, 
const TabletSchemaPB
     if (in.has_commit_tso_col_idx()) {
         out->set_commit_tso_col_idx(in.commit_tso_col_idx());
     }
+    if (in.has_row_lsn_col_idx()) {
+        out->set_row_lsn_col_idx(in.row_lsn_col_idx());
+    }
     if (in.has_binlog_tso_col_idx()) {
         out->set_binlog_tso_col_idx(in.binlog_tso_col_idx());
     }
@@ -565,6 +568,9 @@ void doris_tablet_schema_to_cloud(TabletSchemaCloudPB* out, 
TabletSchemaPB&& in)
     if (in.has_commit_tso_col_idx()) {
         out->set_commit_tso_col_idx(in.commit_tso_col_idx());
     }
+    if (in.has_row_lsn_col_idx()) {
+        out->set_row_lsn_col_idx(in.row_lsn_col_idx());
+    }
     if (in.has_binlog_tso_col_idx()) {
         out->set_binlog_tso_col_idx(in.binlog_tso_col_idx());
     }
@@ -639,6 +645,9 @@ void cloud_tablet_schema_to_doris(TabletSchemaPB* out, 
const TabletSchemaCloudPB
     if (in.has_commit_tso_col_idx()) {
         out->set_commit_tso_col_idx(in.commit_tso_col_idx());
     }
+    if (in.has_row_lsn_col_idx()) {
+        out->set_row_lsn_col_idx(in.row_lsn_col_idx());
+    }
     if (in.has_binlog_tso_col_idx()) {
         out->set_binlog_tso_col_idx(in.binlog_tso_col_idx());
     }
@@ -701,6 +710,9 @@ void cloud_tablet_schema_to_doris(TabletSchemaPB* out, 
TabletSchemaCloudPB&& in)
     if (in.has_commit_tso_col_idx()) {
         out->set_commit_tso_col_idx(in.commit_tso_col_idx());
     }
+    if (in.has_row_lsn_col_idx()) {
+        out->set_row_lsn_col_idx(in.row_lsn_col_idx());
+    }
     if (in.has_binlog_tso_col_idx()) {
         out->set_binlog_tso_col_idx(in.binlog_tso_col_idx());
     }
diff --git a/be/src/exec/sink/writer/vtablet_writer.cpp 
b/be/src/exec/sink/writer/vtablet_writer.cpp
index e1a3a29914d..4fac6ee4e38 100644
--- a/be/src/exec/sink/writer/vtablet_writer.cpp
+++ b/be/src/exec/sink/writer/vtablet_writer.cpp
@@ -937,8 +937,8 @@ Status VNodeChannel::add_block(Block* block, const Payload* 
payload) {
             
_cur_add_block_request->add_tablet_ids(row_part_tablet_ids->tablet_ids[route_idx]);
         }
     }
-    for (auto row_binlog_lsn : payload->row_binlog_lsns) {
-        _cur_add_block_request->add_row_binlog_lsns(row_binlog_lsn);
+    for (auto allocated_lsn : payload->allocated_lsns) {
+        _cur_add_block_request->add_allocated_lsns(allocated_lsn);
     }
     _write_bytes.fetch_add(_cur_mutable_block->bytes());
 
@@ -964,7 +964,7 @@ Status VNodeChannel::add_block(Block* block, const Payload* 
payload) {
         _cur_mutable_block = MutableBlock::create_unique(block->clone_empty());
         _cur_add_block_request->clear_tablet_ids();
         _cur_add_block_request->clear_partition_ids();
-        _cur_add_block_request->clear_row_binlog_lsns();
+        _cur_add_block_request->clear_allocated_lsns();
     }
 
     return Status::OK();
@@ -1644,7 +1644,7 @@ Status VTabletWriter::_init(RuntimeState* state, 
RuntimeProfile* profile) {
     bool has_row_binlog = std::any_of(_schema->indexes().begin(), 
_schema->indexes().end(),
                                       [](const auto* index) { return 
index->row_binlog_id > 0; });
     if (has_row_binlog) {
-        _row_binlog_lsn_buffer = 
GlobalAutoIncBuffers::GetInstance()->get_auto_inc_buffer(
+        _allocated_lsn_buffer = 
GlobalAutoIncBuffers::GetInstance()->get_auto_inc_buffer(
                 _schema->db_id(), _schema->table_id(), kBinlogLsnAutoIncId);
     }
     _schema->set_timestamp_ms(state->timestamp_ms());
@@ -2160,10 +2160,10 @@ Status 
VTabletWriter::_generate_one_index_channel_payload(
 
     size_t row_cnt = row_ids.size();
     bool has_row_binlog = _schema->indexes()[index_idx]->row_binlog_id > 0;
-    std::vector<int64_t> row_binlog_lsns;
+    std::vector<int64_t> allocated_lsns;
     if (has_row_binlog && row_cnt > 0) {
-        DCHECK(_row_binlog_lsn_buffer != nullptr);
-        RETURN_IF_ERROR(allocate_binlog_lsn(_row_binlog_lsn_buffer, row_cnt, 
row_binlog_lsns));
+        DCHECK(_allocated_lsn_buffer != nullptr);
+        RETURN_IF_ERROR(allocate_lsn(_allocated_lsn_buffer, row_cnt, 
allocated_lsns));
     }
 
     for (size_t i = 0; i < row_ids.size(); i++) {
@@ -2185,13 +2185,13 @@ Status 
VTabletWriter::_generate_one_index_channel_payload(
                 payload_it->second.row_ids->reserve(row_cnt);
                 payload_it->second.route_idxs.reserve(row_cnt);
                 if (has_row_binlog) {
-                    payload_it->second.row_binlog_lsns.reserve(row_cnt);
+                    payload_it->second.allocated_lsns.reserve(row_cnt);
                 }
             }
             payload_it->second.row_ids->push_back(row_ids[i]);
             payload_it->second.route_idxs.push_back(cast_set<uint32_t>(i));
             if (has_row_binlog) {
-                
payload_it->second.row_binlog_lsns.push_back(row_binlog_lsns[i]);
+                payload_it->second.allocated_lsns.push_back(allocated_lsns[i]);
             }
             continue;
         }
@@ -2216,13 +2216,13 @@ Status 
VTabletWriter::_generate_one_index_channel_payload(
                 payload_it->second.row_ids->reserve(row_cnt);
                 payload_it->second.route_idxs.reserve(row_cnt);
                 if (has_row_binlog) {
-                    payload_it->second.row_binlog_lsns.reserve(row_cnt);
+                    payload_it->second.allocated_lsns.reserve(row_cnt);
                 }
             }
             payload_it->second.row_ids->push_back(row_ids[i]);
             payload_it->second.route_idxs.push_back(cast_set<uint32_t>(i));
             if (has_row_binlog) {
-                
payload_it->second.row_binlog_lsns.push_back(row_binlog_lsns[i]);
+                payload_it->second.allocated_lsns.push_back(allocated_lsns[i]);
             }
         }
     }
diff --git a/be/src/exec/sink/writer/vtablet_writer.h 
b/be/src/exec/sink/writer/vtablet_writer.h
index 1605faba306..932e8b2b56c 100644
--- a/be/src/exec/sink/writer/vtablet_writer.h
+++ b/be/src/exec/sink/writer/vtablet_writer.h
@@ -226,7 +226,7 @@ struct Payload {
     std::unique_ptr<IColumn::Selector> row_ids;
     RowPartTabletIds* row_part_tablet_ids = nullptr;
     std::vector<uint32_t> route_idxs;
-    std::vector<int64_t> row_binlog_lsns;
+    std::vector<int64_t> allocated_lsns;
 };
 
 // every NodeChannel keeps a data transmission channel with one BE. for 
multiple times open, it has a dozen of requests and corresponding closures.
@@ -723,8 +723,8 @@ private:
     bthread::Mutex _stop_check_channel;
     std::vector<std::shared_ptr<IndexChannel>> _channels;
     std::unordered_map<int64_t, std::shared_ptr<IndexChannel>> 
_index_id_to_channel;
-    // Table-level row-binlog LSN buffer
-    std::shared_ptr<AutoIncIDBuffer> _row_binlog_lsn_buffer;
+    // Table-level LSN buffer
+    std::shared_ptr<AutoIncIDBuffer> _allocated_lsn_buffer;
 
     std::unique_ptr<ThreadPoolToken> _send_batch_thread_pool_token;
 
diff --git a/be/src/load/channel/tablets_channel.cpp 
b/be/src/load/channel/tablets_channel.cpp
index d35b260076e..a589b8ba13e 100644
--- a/be/src/load/channel/tablets_channel.cpp
+++ b/be/src/load/channel/tablets_channel.cpp
@@ -627,14 +627,14 @@ Status BaseTabletsChannel::_write_block_data(
                 print_id(_load_id), _index_id, request.packet_seq(), 
send_data.rows(),
                 request.tablet_ids_size());
     }
-    bool has_row_binlog_lsn = request.row_binlog_lsns_size() > 0;
-    if (has_row_binlog_lsn) {
-        if (send_data.rows() != request.row_binlog_lsns_size()) {
+    bool has_allocated_lsn = request.allocated_lsns_size() > 0;
+    if (has_allocated_lsn) {
+        if (send_data.rows() != request.allocated_lsns_size()) {
             return Status::InternalError(
-                    "invalid add block request row-binlog lsn count, 
load_id={}, index_id={}, "
-                    "packet_seq={}, block_rows={}, row_binlog_lsns_size={}",
+                    "invalid add block request allocated lsn count, 
load_id={}, index_id={}, "
+                    "packet_seq={}, block_rows={}, allocated_lsns_size={}",
                     print_id(_load_id), _index_id, request.packet_seq(), 
send_data.rows(),
-                    request.row_binlog_lsns_size());
+                    request.allocated_lsns_size());
         }
     }
 
@@ -791,10 +791,10 @@ Status 
BaseTabletsChannel::_write_block_data_for_adaptive_random_bucket(
         RETURN_IF_ERROR(_prepare_adaptive_random_bucket_writer(tablet_writer));
 
         TabletAddRowsPayload rows {.row_idxs = row_idxs};
-        if (request.row_binlog_lsns_size() > 0) {
-            rows.row_binlog_lsns.reserve(row_idxs.size());
+        if (request.allocated_lsns_size() > 0) {
+            rows.allocated_lsns.reserve(row_idxs.size());
             for (auto row_idx : row_idxs) {
-                
rows.row_binlog_lsns.emplace_back(request.row_binlog_lsns(row_idx));
+                
rows.allocated_lsns.emplace_back(request.allocated_lsns(row_idx));
             }
         }
         bool memtable_flushed = false;
@@ -920,14 +920,14 @@ void BaseTabletsChannel::_build_tablet_to_rows(
     // tests show that a relatively coarse-grained read lock here performs 
better under multicore scenario
     // see: https://github.com/apache/doris/pull/28552
     std::shared_lock<std::shared_mutex> rlock(_broken_tablets_lock);
-    bool has_row_binlog_lsn = request.row_binlog_lsns_size() > 0;
+    bool has_allocated_lsn = request.allocated_lsns_size() > 0;
     if (request.is_single_tablet_block()) {
         // The cloud mode need the tablet ids to prepare rowsets.
         int64_t tablet_id = request.tablet_ids(0);
         auto& rows = (*tablet_to_rows)[tablet_id];
         rows.row_idxs.emplace_back(0);
-        if (has_row_binlog_lsn) {
-            rows.row_binlog_lsns.emplace_back(request.row_binlog_lsns(0));
+        if (has_allocated_lsn) {
+            rows.allocated_lsns.emplace_back(request.allocated_lsns(0));
         }
         return;
     }
@@ -940,8 +940,8 @@ void BaseTabletsChannel::_build_tablet_to_rows(
         }
         auto& rows = (*tablet_to_rows)[tablet_id];
         rows.row_idxs.emplace_back(i);
-        if (has_row_binlog_lsn) {
-            rows.row_binlog_lsns.emplace_back(request.row_binlog_lsns(i));
+        if (has_allocated_lsn) {
+            rows.allocated_lsns.emplace_back(request.allocated_lsns(i));
         }
     }
 }
diff --git a/be/src/load/delta_writer/delta_writer_context.h 
b/be/src/load/delta_writer/delta_writer_context.h
index 856601c0be6..06b4659c79b 100644
--- a/be/src/load/delta_writer/delta_writer_context.h
+++ b/be/src/load/delta_writer/delta_writer_context.h
@@ -60,7 +60,7 @@ struct WriteRequest {
 
 struct TabletAddRowsPayload {
     DorisVector<uint32_t> row_idxs;
-    DorisVector<int64_t> row_binlog_lsns = {};
+    DorisVector<int64_t> allocated_lsns = {};
 };
 
 } // namespace doris
diff --git a/be/src/load/memtable/memtable.cpp 
b/be/src/load/memtable/memtable.cpp
index b9b31a34a23..2a9ac792f3b 100644
--- a/be/src/load/memtable/memtable.cpp
+++ b/be/src/load/memtable/memtable.cpp
@@ -29,6 +29,7 @@
 #include "bvar/bvar.h"
 #include "common/config.h"
 #include "core/column/column.h"
+#include "core/column/column_vector.h"
 #include "exprs/aggregate/aggregate_function_reader.h"
 #include "exprs/aggregate/aggregate_function_simple_factory.h"
 #include "load/delta_writer/delta_writer_context.h"
@@ -53,14 +54,14 @@ using namespace ErrorCode;
 MemTable::MemTable(int64_t tablet_id, std::shared_ptr<TabletSchema> 
tablet_schema,
                    const std::vector<SlotDescriptor*>* slot_descs, 
TupleDescriptor* tuple_desc,
                    bool enable_unique_key_mow, PartialUpdateInfo* 
partial_update_info,
-                   const std::shared_ptr<ResourceContext>& resource_ctx, bool 
need_row_binlog_lsn)
+                   const std::shared_ptr<ResourceContext>& resource_ctx, bool 
need_lsn)
         : _mem_type(MemType::ACTIVE),
           _tablet_id(tablet_id),
           _enable_unique_key_mow(enable_unique_key_mow),
           _keys_type(tablet_schema->keys_type()),
           _tablet_schema(tablet_schema),
           _resource_ctx(resource_ctx),
-          _need_row_binlog_lsn(need_row_binlog_lsn),
+          _need_lsn(need_lsn),
           _is_first_insertion(true),
           _agg_functions(tablet_schema->num_columns()),
           _offsets_of_aggregate_states(tablet_schema->num_columns()),
@@ -81,6 +82,7 @@ MemTable::MemTable(int64_t tablet_id, 
std::shared_ptr<TabletSchema> tablet_schem
         }
     }
     _init_columns_offset_by_slot_descs(slot_descs, tuple_desc);
+    _row_lsn_col_pos = tablet_schema->row_lsn_col_idx();
     // TODO: Support ZOrderComparator in the future
     _row_in_blocks = 
std::make_unique<DorisVector<std::shared_ptr<RowInBlock>>>();
     _load_mem_limit = MemInfo::mem_limit() * 
config::load_process_max_memory_limit_percent / 100;
@@ -187,7 +189,7 @@ MemTable::~MemTable() {
         _input_mutable_block.clear();
         _output_mutable_block.clear();
         // Reset the LSN sidecar to release its capacity tracked by the 
memtable tracker.
-        _output_row_binlog_lsns = DorisVector<int64_t>();
+        _output_allocated_lsns = std::make_shared<std::vector<int64_t>>();
     }
     if (_is_flush_success) {
         // If the memtable is flush success, then its memtracker's consumption 
should be 0
@@ -208,19 +210,19 @@ Status MemTable::insert(const Block* input_block, const 
TabletAddRowsPayload& ro
             _resource_ctx->memory_context()->mem_tracker()->write_tracker());
     SCOPED_CONSUME_MEM_TRACKER(_mem_tracker);
     const auto& row_idxs = rows.row_idxs;
-    const auto& row_binlog_lsns = rows.row_binlog_lsns;
+    const auto& allocated_lsns = rows.allocated_lsns;
 
-    if (_need_row_binlog_lsn) {
-        if (row_binlog_lsns.empty()) {
+    if (_need_lsn) {
+        if (allocated_lsns.empty()) {
             return Status::InternalError(
-                    "row binlog lsn is missing for memtable insert, "
+                    "allocated lsn is missing for memtable insert, "
                     "tablet_id={}",
                     _tablet_id);
         }
-        DCHECK_EQ(row_binlog_lsns.size(), row_idxs.size());
-    } else if (!row_binlog_lsns.empty()) {
+        DCHECK_EQ(allocated_lsns.size(), row_idxs.size());
+    } else if (!allocated_lsns.empty()) {
         return Status::InternalError(
-                "row binlog lsn is unexpectedly provided for memtable insert, 
tablet_id={}",
+                "allocated lsn is unexpectedly provided for memtable insert, 
tablet_id={}",
                 _tablet_id);
     }
 
@@ -266,30 +268,36 @@ Status MemTable::insert(const Block* input_block, const 
TabletAddRowsPayload& ro
     size_t cursor_in_mutableblock = _input_mutable_block.rows();
     RETURN_IF_ERROR(_input_mutable_block.add_rows(input_block, row_idxs.data(),
                                                   row_idxs.data() + num_rows, 
&_column_offset));
+    if (_need_lsn && _row_lsn_col_pos >= 0) {
+        auto lsn_column = ColumnInt64::create();
+        lsn_column->get_data().assign(allocated_lsns.begin(), 
allocated_lsns.end());
+        _input_mutable_block.get_column_by_position(_row_lsn_col_pos)
+                ->replace_column_data_range(*lsn_column, 0, num_rows, 
cursor_in_mutableblock);
+    }
     for (int i = 0; i < num_rows; i++) {
         _row_in_blocks->emplace_back(std::make_shared<RowInBlock>(
-                cursor_in_mutableblock + i, _need_row_binlog_lsn ? 
row_binlog_lsns[i] : 0));
+                cursor_in_mutableblock + i, _need_lsn ? allocated_lsns[i] : 
0));
     }
 
     _stat.raw_rows += num_rows;
     return Status::OK();
 }
 
-void MemTable::_merge_row_binlog_lsn(RowInBlock* src_row, RowInBlock* dst_row) 
{
-    if (_need_row_binlog_lsn) {
-        dst_row->_row_binlog_lsn = std::max(dst_row->_row_binlog_lsn, 
src_row->_row_binlog_lsn);
+void MemTable::_merge_allocated_lsn(RowInBlock* src_row, RowInBlock* dst_row) {
+    if (_need_lsn) {
+        dst_row->_allocated_lsn = std::max(dst_row->_allocated_lsn, 
src_row->_allocated_lsn);
     }
 }
 
-void MemTable::_append_output_row_binlog_lsn(RowInBlock* row) {
-    if (_need_row_binlog_lsn) {
-        _output_row_binlog_lsns.emplace_back(row->_row_binlog_lsn);
+void MemTable::_append_output_allocated_lsn(RowInBlock* row) {
+    if (_need_lsn) {
+        _output_allocated_lsns->emplace_back(row->_allocated_lsn);
     }
 }
 
 void MemTable::_aggregate_two_row_with_sequence_map(MutableBlock& 
mutable_block,
                                                     RowInBlock* src_row, 
RowInBlock* dst_row) {
-    _merge_row_binlog_lsn(src_row, dst_row);
+    _merge_allocated_lsn(src_row, dst_row);
     // for each mapping replace value columns according to the sequence column 
compare result
     // for example: a b c d s1 s2  (key:a , s1=>[b,c], s2=>[d])
     // src row:     1 4 5 6  8 9
@@ -329,7 +337,7 @@ void 
MemTable::_aggregate_two_row_with_sequence_map(MutableBlock& mutable_block,
 template <bool has_skip_bitmap_col>
 void MemTable::_aggregate_two_row_in_block(MutableBlock& mutable_block, 
RowInBlock* src_row,
                                            RowInBlock* dst_row) {
-    _merge_row_binlog_lsn(src_row, dst_row);
+    _merge_allocated_lsn(src_row, dst_row);
     // for flexible partial update, the caller must guarantees that either 
src_row and dst_row
     // both specify the sequence column, or src_row and dst_row both don't 
specify the
     // sequence column
@@ -378,12 +386,12 @@ Status MemTable::_put_into_output(Block& in_block) {
     DorisVector<uint32_t> row_pos_vec;
     DCHECK(in_block.rows() <= std::numeric_limits<int>::max());
     row_pos_vec.reserve(in_block.rows());
-    if (_need_row_binlog_lsn) {
-        _output_row_binlog_lsns.reserve(_output_row_binlog_lsns.size() + 
in_block.rows());
+    if (_need_lsn) {
+        _output_allocated_lsns->reserve(_output_allocated_lsns->size() + 
in_block.rows());
     }
     for (int i = 0; i < _row_in_blocks->size(); i++) {
         row_pos_vec.emplace_back((*_row_in_blocks)[i]->_row_pos);
-        _append_output_row_binlog_lsn((*_row_in_blocks)[i].get());
+        _append_output_allocated_lsn((*_row_in_blocks)[i].get());
     }
     return _output_mutable_block.add_rows(&in_block, row_pos_vec.data(),
                                           row_pos_vec.data() + 
in_block.rows());
@@ -442,17 +450,17 @@ Status MemTable::_sort_by_cluster_keys() {
 
     DorisVector<std::shared_ptr<RowInBlock>> row_in_blocks;
     row_in_blocks.reserve(mutable_block.rows());
-    if (_need_row_binlog_lsn) {
-        DCHECK_EQ(_output_row_binlog_lsns.size(), mutable_block.rows());
+    if (_need_lsn) {
+        DCHECK_EQ(_output_allocated_lsns->size(), mutable_block.rows());
     }
     for (size_t i = 0; i < mutable_block.rows(); i++) {
         row_in_blocks.emplace_back(
-                _need_row_binlog_lsn ? std::make_shared<RowInBlock>(i, 
_output_row_binlog_lsns[i])
-                                     : std::make_shared<RowInBlock>(i));
+                _need_lsn ? std::make_shared<RowInBlock>(i, 
(*_output_allocated_lsns)[i])
+                          : std::make_shared<RowInBlock>(i));
     }
-    if (_need_row_binlog_lsn) {
-        _output_row_binlog_lsns.clear();
-        _output_row_binlog_lsns.reserve(mutable_block.rows());
+    if (_need_lsn) {
+        _output_allocated_lsns = std::make_shared<std::vector<int64_t>>();
+        _output_allocated_lsns->reserve(mutable_block.rows());
     }
     Tie tie = Tie(0, mutable_block.rows());
 
@@ -484,7 +492,7 @@ Status MemTable::_sort_by_cluster_keys() {
     row_pos_vec.reserve(in_block.rows());
     for (int i = 0; i < row_in_blocks.size(); i++) {
         row_pos_vec.emplace_back(row_in_blocks[i]->_row_pos);
-        _append_output_row_binlog_lsn(row_in_blocks[i].get());
+        _append_output_allocated_lsn(row_in_blocks[i].get());
     }
     std::vector<int> column_offset;
     for (int i = 0; i < _column_offset.size(); ++i) {
@@ -548,7 +556,7 @@ void MemTable::_finalize_one_row(RowInBlock* row, 
MutableBlock& mutable_block, i
                     *mutable_block.get_column_by_position(i), row->_row_pos);
         }
     }
-    _append_output_row_binlog_lsn(row);
+    _append_output_allocated_lsn(row);
     if constexpr (!is_final) {
         row->_row_pos = row_pos;
     }
@@ -641,7 +649,7 @@ void MemTable::_aggregate() {
         //TODO(weixang):opt here.
         _output_mutable_block = 
MutableBlock::build_mutable_block(std::move(*empty_input_block));
         _output_mutable_block.clear_column_data();
-        _output_row_binlog_lsns.clear();
+        _output_allocated_lsns = std::make_shared<std::vector<int64_t>>();
         *_row_in_blocks = temp_row_in_blocks;
         _last_sorted_pos = _row_in_blocks->size();
     }
@@ -695,7 +703,7 @@ void 
MemTable::_aggregate_for_flexible_partial_update_without_seq_col(
             if (cur_row_has_delete_sign) {
                 if (row_without_delete_sign != nullptr) {
                     // if there exits row without delete sign, remove it first
-                    _merge_row_binlog_lsn(row_without_delete_sign.get(), 
cur_row);
+                    _merge_allocated_lsn(row_without_delete_sign.get(), 
cur_row);
                     _clear_row_agg(row_without_delete_sign.get());
                     _stat.merged_rows++;
                     row_without_delete_sign = nullptr;
@@ -806,15 +814,15 @@ size_t MemTable::get_flush_reserve_memory_size() const {
 }
 
 Status MemTable::_to_block(std::unique_ptr<Block>* res) {
-    _output_row_binlog_lsns.clear();
+    _output_allocated_lsns = std::make_shared<std::vector<int64_t>>();
     size_t same_keys_num = _sort();
     if (_keys_type == KeysType::DUP_KEYS || same_keys_num == 0) {
         if (_keys_type == KeysType::DUP_KEYS && 
_tablet_schema->num_key_columns() == 0) {
             _output_mutable_block.swap(_input_mutable_block);
-            if (_need_row_binlog_lsn) {
-                _output_row_binlog_lsns.reserve(_row_in_blocks->size());
+            if (_need_lsn) {
+                _output_allocated_lsns->reserve(_row_in_blocks->size());
                 for (const auto& row : *_row_in_blocks) {
-                    _append_output_row_binlog_lsn(row.get());
+                    _append_output_allocated_lsn(row.get());
                 }
             }
         } else {
@@ -832,8 +840,8 @@ Status MemTable::_to_block(std::unique_ptr<Block>* res) {
         }
         RETURN_IF_ERROR(_sort_by_cluster_keys());
     }
-    if (_need_row_binlog_lsn) {
-        DCHECK_EQ(_output_row_binlog_lsns.size(), 
_output_mutable_block.rows());
+    if (_need_lsn) {
+        DCHECK_EQ(_output_allocated_lsns->size(), 
_output_mutable_block.rows());
     }
     _input_mutable_block.clear();
     *res = Block::create_unique(_output_mutable_block.to_block());
diff --git a/be/src/load/memtable/memtable.h b/be/src/load/memtable/memtable.h
index 5e7c60a31dd..4228da6b928 100644
--- a/be/src/load/memtable/memtable.h
+++ b/be/src/load/memtable/memtable.h
@@ -34,6 +34,7 @@
 #include "runtime/memory/mem_tracker.h"
 #include "runtime/thread_context.h"
 #include "storage/partial_update_info.h"
+#include "storage/rowset/rowset_fwd.h"
 #include "storage/tablet/tablet_schema.h"
 
 namespace doris {
@@ -52,14 +53,14 @@ enum MemType { ACTIVE = 0, WRITE_FINISHED = 1, FLUSH = 2 };
 // row pos in _input_mutable_block
 struct RowInBlock {
     size_t _row_pos;
-    int64_t _row_binlog_lsn = 0;
+    int64_t _allocated_lsn = 0;
     char* _agg_mem = nullptr;
     size_t* _agg_state_offset = nullptr;
     bool _has_init_agg;
 
     RowInBlock(size_t row) : _row_pos(row), _has_init_agg(false) {}
-    RowInBlock(size_t row, int64_t row_binlog_lsn)
-            : _row_pos(row), _row_binlog_lsn(row_binlog_lsn), 
_has_init_agg(false) {}
+    RowInBlock(size_t row, int64_t allocated_lsn)
+            : _row_pos(row), _allocated_lsn(allocated_lsn), 
_has_init_agg(false) {}
 
     void init_agg_places(char* agg_mem, size_t* agg_state_offset) {
         _has_init_agg = true;
@@ -177,8 +178,7 @@ public:
     MemTable(int64_t tablet_id, std::shared_ptr<TabletSchema> tablet_schema,
              const std::vector<SlotDescriptor*>* slot_descs, TupleDescriptor* 
tuple_desc,
              bool enable_unique_key_mow, PartialUpdateInfo* 
partial_update_info,
-             const std::shared_ptr<ResourceContext>& resource_ctx,
-             bool need_row_binlog_lsn = false);
+             const std::shared_ptr<ResourceContext>& resource_ctx, bool 
need_lsn = false);
     ~MemTable();
 
     int64_t tablet_id() const { return _tablet_id; }
@@ -195,7 +195,7 @@ public:
 
     Status to_block(std::unique_ptr<Block>* res);
 
-    const DorisVector<int64_t>& row_binlog_lsns() const { return 
_output_row_binlog_lsns; }
+    ConstAllocatedLsnVectorSharedPtr allocated_lsns() const { return 
_output_allocated_lsns; }
 
     bool empty() const { return _input_mutable_block.rows() == 0; }
 
@@ -219,12 +219,12 @@ private:
     void _aggregate_two_row_in_block(MutableBlock& mutable_block, RowInBlock* 
new_row,
                                      RowInBlock* row_in_skiplist);
 
-    // Merge row-binlog LSN sidecar only when MemTable merges two RowInBlock 
objects.
+    // Merge allocated LSN sidecar only when MemTable merges two RowInBlock 
objects.
     // Table models that require complex merge semantics, such as AGG tables 
and unique key
-    // merge-on-read tables, do not support row-binlog LSN now and are 
rejected in insert().
-    void _merge_row_binlog_lsn(RowInBlock* src_row, RowInBlock* dst_row);
+    // merge-on-read tables, do not support allocated LSN now and are rejected 
in insert().
+    void _merge_allocated_lsn(RowInBlock* src_row, RowInBlock* dst_row);
 
-    void _append_output_row_binlog_lsn(RowInBlock* row);
+    void _append_output_allocated_lsn(RowInBlock* row);
 
     void _aggregate_two_row_with_sequence_map(MutableBlock& mutable_block, 
RowInBlock* new_row,
                                               RowInBlock* row_in_skiplist);
@@ -257,6 +257,7 @@ private:
     void _init_columns_offset_by_slot_descs(const 
std::vector<SlotDescriptor*>* slot_descs,
                                             const TupleDescriptor* tuple_desc);
     std::vector<int> _column_offset;
+    int32_t _row_lsn_col_pos = -1;
 
     // Number of rows inserted to this memtable.
     // This is not the rows in this memtable, because rows may be merged
@@ -266,8 +267,8 @@ private:
     //for vectorized
     MutableBlock _input_mutable_block;
     MutableBlock _output_mutable_block;
-    DorisVector<int64_t> _output_row_binlog_lsns;
-    bool _need_row_binlog_lsn = false;
+    AllocatedLsnVectorSharedPtr _output_allocated_lsns = 
std::make_shared<std::vector<int64_t>>();
+    bool _need_lsn = false;
     size_t _last_sorted_pos = 0;
     size_t _last_agg_pos = 0;
 
diff --git a/be/src/load/memtable/memtable_flush_executor.cpp 
b/be/src/load/memtable/memtable_flush_executor.cpp
index 7a3c7cf82be..1970d074a5e 100644
--- a/be/src/load/memtable/memtable_flush_executor.cpp
+++ b/be/src/load/memtable/memtable_flush_executor.cpp
@@ -119,6 +119,10 @@ SharedMemtable::~SharedMemtable() {
     if (block == nullptr) {
         return;
     }
+    if (has_allocated_lsns) {
+        DCHECK(rowset_ctx != nullptr);
+        rowset_ctx->remove_segment_allocated_lsns(segment_id);
+    }
     DCHECK(memtable != nullptr);
     SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(
             
memtable->resource_ctx()->memory_context()->mem_tracker()->write_tracker());
@@ -183,6 +187,8 @@ Status FlushToken::submit(std::shared_ptr<MemTable> 
mem_table) {
 
         shared_memtable = std::make_shared<SharedMemtable>();
         shared_memtable->memtable = mem_table;
+        shared_memtable->rowset_ctx =
+                
const_cast<RowsetWriterContext*>(&group_rowset_writer->context());
         // Keep data/binlog segment_id allocators in sync.
         auto segment_id = DORIS_TRY(data_writer->allocate_segment_id());
         auto binlog_segment_id = 
DORIS_TRY(binlog_writer->allocate_segment_id());
@@ -308,6 +314,15 @@ Status FlushToken::_memtable2block(MemTable* memtable, 
SharedMemtable* shared_me
         shared_memtable->block_status = memtable->to_block(&block);
         if (shared_memtable->block_status.ok()) {
             shared_memtable->block.reset(block.release());
+            auto* rowset_ctx = shared_memtable->rowset_ctx;
+            DCHECK(rowset_ctx != nullptr);
+            if (rowset_ctx->need_allocated_lsn() && 
shared_memtable->block->rows() > 0) {
+                auto memtable_lsns = memtable->allocated_lsns();
+                DCHECK_EQ(memtable_lsns->size(), 
shared_memtable->block->rows());
+                
rowset_ctx->insert_segment_allocated_lsns(shared_memtable->segment_id,
+                                                          memtable_lsns);
+                shared_memtable->has_allocated_lsns = true;
+            }
         }
     });
     if (!shared_memtable->block_status.ok()) {
@@ -392,16 +407,6 @@ void FlushToken::_flush_memtable_impl(RowsetWriter* 
flush_writer, MemTable* memt
             // }};
             std::shared_ptr<Block> flush_block;
             RETURN_IF_ERROR(_memtable2block(memtable, shared_memtable, 
flush_block));
-            if (flush_writer->context().write_binlog_opt().enable && 
flush_block->rows() > 0) {
-                const auto& memtable_lsns = memtable->row_binlog_lsns();
-                DCHECK_EQ(memtable_lsns.size(), flush_block->rows());
-                auto lsn_ids = 
std::make_shared<std::vector<int64_t>>(memtable_lsns.begin(),
-                                                                      
memtable_lsns.end());
-                const_cast<RowsetWriterContext&>(flush_writer->context())
-                        .write_binlog_opt()
-                        .write_binlog_config()
-                        .insert_seg_lsn(segment_id, std::move(lsn_ids));
-            }
             RETURN_IF_ERROR(
                     flush_writer->flush_memtable(flush_block.get(), 
segment_id, &flush_size));
             memtable->set_flush_success();
diff --git a/be/src/load/memtable/memtable_flush_executor.h 
b/be/src/load/memtable/memtable_flush_executor.h
index dbe997cfceb..c66051cc5f7 100644
--- a/be/src/load/memtable/memtable_flush_executor.h
+++ b/be/src/load/memtable/memtable_flush_executor.h
@@ -38,6 +38,7 @@ class MemTableMemoryLimiter;
 class Block;
 class GroupRowsetWriter;
 class OlapTableSchemaParam;
+struct RowsetWriterContext;
 class RowsetWriter;
 class SystemMetrics;
 class WorkloadGroup;
@@ -63,6 +64,8 @@ struct SharedMemtable {
     std::once_flag block_once;
     Status block_status;
     std::shared_ptr<Block> block;
+    RowsetWriterContext* rowset_ctx = nullptr;
+    bool has_allocated_lsns = false;
 
     std::atomic<int> finished_sub_task_count {0};
     // data + binlog
diff --git a/be/src/load/memtable/memtable_writer.cpp 
b/be/src/load/memtable/memtable_writer.cpp
index 5a6eb301cf1..63f399fba01 100644
--- a/be/src/load/memtable/memtable_writer.cpp
+++ b/be/src/load/memtable/memtable_writer.cpp
@@ -83,25 +83,26 @@ Status MemTableWriter::init(std::shared_ptr<RowsetWriter> 
rowset_writer,
     _unique_key_mow = unique_key_mow;
     _partial_update_info = partial_update_info;
     _resource_ctx = thread_context()->resource_ctx();
-    _need_row_binlog_lsn = false;
+    // Only the row-binlog base index needs LSN, aligned with the sink 
(row_binlog_id > 0).
+    _need_lsn = false;
     if (_req.table_schema_param != nullptr) {
         for (const auto* index_schema : _req.table_schema_param->indexes()) {
             if (index_schema->index_id == _req.index_id) {
-                _need_row_binlog_lsn = index_schema->row_binlog_id > 0;
+                _need_lsn = index_schema->row_binlog_id > 0;
                 break;
             }
         }
     }
-    if (_need_row_binlog_lsn) {
+    if (_need_lsn) {
         const auto keys_type = _tablet_schema->keys_type();
         if (keys_type == KeysType::AGG_KEYS ||
             (keys_type == KeysType::UNIQUE_KEYS && !_unique_key_mow)) {
             // Row-binlog LSN sidecar does not support MemTable aggregation 
now. For AGG tables
             // and unique key merge-on-read tables, multiple input rows can be 
merged into one
             // output row in MemTable, so their output LSN semantics should be 
implemented
-            // explicitly before enabling row-binlog LSNs on these table types.
+            // explicitly before enabling allocated LSNs on these table types.
             return Status::NotSupported(
-                    "row binlog lsn does not support AGG table or unique key 
merge-on-read table");
+                    "allocated lsn does not support AGG table or unique key 
merge-on-read table");
         }
     }
 
@@ -125,7 +126,7 @@ Status MemTableWriter::write(const Block* block, const 
TabletAddRowsPayload& row
         *memtable_flushed = false;
     }
     if (UNLIKELY(rows.row_idxs.empty())) {
-        DCHECK(rows.row_binlog_lsns.empty());
+        DCHECK(rows.allocated_lsns.empty());
         return Status::OK();
     }
     _lock_watch.start();
@@ -142,13 +143,13 @@ Status MemTableWriter::write(const Block* block, const 
TabletAddRowsPayload& row
                                              _req.tablet_id, 
_req.load_id.hi(), _req.load_id.lo());
     }
 
-    if (_need_row_binlog_lsn) {
-        if (rows.row_binlog_lsns.empty()) {
+    if (_need_lsn) {
+        if (rows.allocated_lsns.empty()) {
             return Status::InternalError(
-                    "row binlog lsn is missing for tablet_id={}, index_id={}, 
load_id={}-{}",
+                    "allocated lsn is missing for tablet_id={}, index_id={}, 
load_id={}-{}",
                     _req.tablet_id, _req.index_id, _req.load_id.hi(), 
_req.load_id.lo());
         }
-        DCHECK(rows.row_binlog_lsns.size() == rows.row_idxs.size());
+        DCHECK(rows.allocated_lsns.size() == rows.row_idxs.size());
     }
 
     // Flush and reset memtable if it is raw rows great than int32_t.
@@ -269,7 +270,7 @@ void MemTableWriter::_reset_mem_table() {
         std::lock_guard<std::mutex> l(_mem_table_ptr_lock);
         _mem_table.reset(new MemTable(_req.tablet_id, _tablet_schema, 
_req.slots, _req.tuple_desc,
                                       _unique_key_mow, 
_partial_update_info.get(), _resource_ctx,
-                                      _need_row_binlog_lsn));
+                                      _need_lsn));
     }
 
     _segment_num++;
diff --git a/be/src/load/memtable/memtable_writer.h 
b/be/src/load/memtable/memtable_writer.h
index aa59a7fae6e..90e9ba564fa 100644
--- a/be/src/load/memtable/memtable_writer.h
+++ b/be/src/load/memtable/memtable_writer.h
@@ -124,7 +124,7 @@ private:
     std::shared_ptr<MemTable> _mem_table;
     TabletSchemaSPtr _tablet_schema;
     bool _unique_key_mow = false;
-    bool _need_row_binlog_lsn = false;
+    bool _need_lsn = false;
 
     // This variable is accessed from writer thread and token flush thread
     // use a shared ptr to avoid use after free problem.
diff --git a/be/src/storage/binlog.h b/be/src/storage/binlog.h
index 097b1e6a5ca..b59eded10ce 100644
--- a/be/src/storage/binlog.h
+++ b/be/src/storage/binlog.h
@@ -32,7 +32,8 @@
 #include "common/status.h"
 #include "exec/sink/autoinc_buffer.h"
 #include "storage/olap_common.h"
-#include "storage/olap_define.h"          // DataWriteType
+#include "storage/olap_define.h" // DataWriteType
+#include "storage/rowset/rowset_fwd.h"
 #include "storage/tablet/tablet_fwd.h"    // BaseTabletSPtr
 #include "storage/tablet/tablet_schema.h" // TabletSchemaSPtr
 #include "storage/utils.h"                // BINLOG_*_COL
@@ -136,12 +137,12 @@ inline std::string 
get_binlog_data_key_from_meta_key(const std::string_view meta
     return fmt::format("{}data_{}", kBinlogPrefix, 
meta_key.substr(kBinlogMetaPrefix.length()));
 }
 
-// Allocate per-row LSNs for row-binlog data.
+// Allocate per-row LSNs.
 // The caller must provide a valid auto-inc buffer (typically from 
GlobalAutoIncBuffers).
-inline Status allocate_binlog_lsn(const std::shared_ptr<AutoIncIDBuffer>& 
lsn_buffer,
-                                  size_t num_rows, std::vector<int64_t>& 
lsn_ids) {
+inline Status allocate_lsn(const std::shared_ptr<AutoIncIDBuffer>& lsn_buffer, 
size_t num_rows,
+                           std::vector<int64_t>& lsn_ids) {
     if (lsn_buffer == nullptr) {
-        return Status::InternalError("binlog<row> try to get lsn buffer but 
null");
+        return Status::InternalError("try to get lsn buffer but null");
     }
     DCHECK(num_rows > 0);
 
@@ -168,23 +169,23 @@ inline int64_t extract_tso_physical_time(int64_t tso) {
 
 namespace segment_v2 {
 
-class SegmentWriteBinlogLsnMap {
+class SegmentAllocatedLsnMap {
 public:
-    void insert_seg_lsn(int64_t seg_id, std::shared_ptr<std::vector<int64_t>> 
lsn_ids) {
+    void insert_segment_allocated_lsns(int64_t seg_id, 
ConstAllocatedLsnVectorSharedPtr lsn_ids) {
         std::lock_guard<std::mutex> l(_mutex);
         _seg_id_to_lsn_ids.emplace(seg_id, std::move(lsn_ids));
     }
 
-    void remove_seg(int64_t seg_id) {
+    void remove_segment(int64_t seg_id) {
         std::lock_guard<std::mutex> l(_mutex);
         _seg_id_to_lsn_ids.erase(seg_id);
     }
 
-    std::shared_ptr<const std::vector<int64_t>> get_seg_lsn(int64_t seg_id) 
const {
+    ConstAllocatedLsnVectorSharedPtr get_segment_allocated_lsns(int64_t 
seg_id) const {
         std::lock_guard<std::mutex> l(_mutex);
         auto it = _seg_id_to_lsn_ids.find(seg_id);
-        CHECK(it != _seg_id_to_lsn_ids.end())
-                << "SegmentWriteBinlogLsnMap::get_seg_lsn missing seg_id=" << 
seg_id
+        DCHECK(it != _seg_id_to_lsn_ids.end())
+                << "SegmentAllocatedLsnMap::get_segment_allocated_lsns missing 
seg_id=" << seg_id
                 << ", existing_seg_ids=[" << ([&] {
                        std::string s;
                        for (const auto& [id, _] : _seg_id_to_lsn_ids) {
@@ -201,7 +202,7 @@ public:
 
 private:
     mutable std::mutex _mutex;
-    std::map<int64_t, std::shared_ptr<std::vector<int64_t>>> 
_seg_id_to_lsn_ids;
+    std::map<int64_t, ConstAllocatedLsnVectorSharedPtr> _seg_id_to_lsn_ids;
 };
 
 struct SegmentWriteBinlogOptions {
@@ -219,26 +220,6 @@ public:
         // input rowset's row-binlog, read LSN from it at publish time
         RowsetSharedPtr row_binlog_rowset = nullptr;
     } source;
-
-    void insert_seg_lsn(int64_t seg_id, std::shared_ptr<std::vector<int64_t>> 
lsn_ids) {
-        DCHECK(lsn_map != nullptr);
-        lsn_map->insert_seg_lsn(seg_id, std::move(lsn_ids));
-    }
-
-    void remove_seg(int64_t seg_id) {
-        DCHECK(lsn_map != nullptr);
-        lsn_map->remove_seg(seg_id);
-    }
-
-    std::shared_ptr<const std::vector<int64_t>> get_seg_lsn(int64_t seg_id) 
const {
-        DCHECK(lsn_map != nullptr);
-        return lsn_map->get_seg_lsn(seg_id);
-    }
-
-    // Shared LSN storage for row-binlog writers.
-    // Keep it as a pointer so SegmentWriteBinlogOptions stays copyable.
-    std::shared_ptr<SegmentWriteBinlogLsnMap> lsn_map =
-            std::make_shared<SegmentWriteBinlogLsnMap>();
 };
 } // namespace segment_v2
 
diff --git a/be/src/storage/rowset/beta_rowset_writer.cpp 
b/be/src/storage/rowset/beta_rowset_writer.cpp
index 9a19e30fe7e..c2033c7661d 100644
--- a/be/src/storage/rowset/beta_rowset_writer.cpp
+++ b/be/src/storage/rowset/beta_rowset_writer.cpp
@@ -368,6 +368,9 @@ BetaRowsetWriter::~BetaRowsetWriter() {
 Status BaseBetaRowsetWriter::init(const RowsetWriterContext& 
rowset_writer_context) {
     _context = rowset_writer_context;
     DCHECK(_context.tablet_schema != nullptr);
+    // Row-binlog writer or a schema carrying ROW_LSN_COL needs allocated LSN.
+    _context._need_allocate_lsn =
+            _context.write_binlog_opt().enable || 
_context.tablet_schema->row_lsn_col_idx() >= 0;
     _rowset_meta.reset(new RowsetMeta);
     if (_context.storage_resource) {
         _rowset_meta->set_remote_storage_resource(*_context.storage_resource);
diff --git a/be/src/storage/rowset/beta_rowset_writer_v2.cpp 
b/be/src/storage/rowset/beta_rowset_writer_v2.cpp
index 0f626e2f5da..0f767f9128e 100644
--- a/be/src/storage/rowset/beta_rowset_writer_v2.cpp
+++ b/be/src/storage/rowset/beta_rowset_writer_v2.cpp
@@ -61,6 +61,10 @@ BetaRowsetWriterV2::~BetaRowsetWriterV2() = default;
 
 Status BetaRowsetWriterV2::init(const RowsetWriterContext& 
rowset_writer_context) {
     _context = rowset_writer_context;
+    // Row-binlog writer or a schema carrying ROW_LSN_COL needs allocated LSN.
+    _context._need_allocate_lsn =
+            _context.write_binlog_opt().enable ||
+            (_context.tablet_schema != nullptr && 
_context.tablet_schema->row_lsn_col_idx() >= 0);
     _context.segment_collector = 
std::make_shared<SegmentCollectorT<BetaRowsetWriterV2>>(this);
     _context.file_writer_creator = 
std::make_shared<FileWriterCreatorT<BetaRowsetWriterV2>>(this);
     return Status::OK();
diff --git a/be/src/storage/rowset/group_rowset_writer.cpp 
b/be/src/storage/rowset/group_rowset_writer.cpp
index a02a9d81272..f905665811d 100644
--- a/be/src/storage/rowset/group_rowset_writer.cpp
+++ b/be/src/storage/rowset/group_rowset_writer.cpp
@@ -32,6 +32,24 @@ void GroupRowsetWriter::set_row_binlog_writer(
     _row_binlog_rowset_writer = row_binlog_rowset_writer;
 }
 
+Status GroupRowsetWriter::init(const RowsetWriterContext& 
rowset_writer_context) {
+    DCHECK(_txn_rowset_writer != nullptr);
+    DCHECK(_row_binlog_rowset_writer != nullptr);
+
+    _context = rowset_writer_context;
+    auto& data_ctx = 
const_cast<RowsetWriterContext&>(_txn_rowset_writer->context());
+    auto& row_binlog_ctx = 
const_cast<RowsetWriterContext&>(_row_binlog_rowset_writer->context());
+    _context._need_allocate_lsn =
+            data_ctx.need_allocated_lsn() || 
row_binlog_ctx.need_allocated_lsn();
+    if (_context.need_allocated_lsn()) {
+        // Share segment LSNs between data and row-binlog writers.
+        _context.allocated_lsn_map = 
std::make_shared<segment_v2::SegmentAllocatedLsnMap>();
+        data_ctx.allocated_lsn_map = _context.allocated_lsn_map;
+        row_binlog_ctx.allocated_lsn_map = _context.allocated_lsn_map;
+    }
+    return Status::OK();
+}
+
 Status GroupRowsetWriter::flush_rowsets() {
     RETURN_IF_ERROR(_txn_rowset_writer->flush());
     RETURN_IF_ERROR(_row_binlog_rowset_writer->flush());
diff --git a/be/src/storage/rowset/group_rowset_writer.h 
b/be/src/storage/rowset/group_rowset_writer.h
index 8aa2672b7ec..4c0520067ff 100644
--- a/be/src/storage/rowset/group_rowset_writer.h
+++ b/be/src/storage/rowset/group_rowset_writer.h
@@ -39,10 +39,7 @@ public:
 
     RowsetWriterSharedPtr data_writer() { return _txn_rowset_writer; }
 
-    Status init(const RowsetWriterContext& rowset_writer_context) override {
-        _context = rowset_writer_context;
-        return Status::OK();
-    }
+    Status init(const RowsetWriterContext& rowset_writer_context) override;
 
     Status add_block(const Block* block) override {
         return Status::Error<ErrorCode::NOT_IMPLEMENTED_ERROR>(
diff --git a/be/src/storage/rowset/rowset_fwd.h 
b/be/src/storage/rowset/rowset_fwd.h
index 09778025e96..91f4b54df9d 100644
--- a/be/src/storage/rowset/rowset_fwd.h
+++ b/be/src/storage/rowset/rowset_fwd.h
@@ -19,6 +19,7 @@
 
 #include <memory>
 #include <unordered_map>
+#include <vector>
 
 namespace doris {
 
@@ -36,5 +37,7 @@ class RowsetWriter;
 using RowsetWriterSharedPtr = std::shared_ptr<RowsetWriter>;
 class RowsetBuilder;
 using RowsetBuilderSharedPtr = std::shared_ptr<RowsetBuilder>;
+using AllocatedLsnVectorSharedPtr = std::shared_ptr<std::vector<int64_t>>;
+using ConstAllocatedLsnVectorSharedPtr = std::shared_ptr<const 
std::vector<int64_t>>;
 
 } // namespace doris
diff --git a/be/src/storage/rowset/rowset_writer_context.h 
b/be/src/storage/rowset/rowset_writer_context.h
index 2390aadeb89..6c36c69ca07 100644
--- a/be/src/storage/rowset/rowset_writer_context.h
+++ b/be/src/storage/rowset/rowset_writer_context.h
@@ -21,9 +21,12 @@
 #include <glog/logging.h>
 
 #include <functional>
+#include <memory>
+#include <mutex>
 #include <optional>
 #include <string_view>
 #include <unordered_map>
+#include <vector>
 
 #include "cloud/config.h"
 #include "common/status.h"
@@ -180,6 +183,29 @@ struct RowsetWriterContext {
 
     std::string job_id;
 
+    // Per-segment LSNs allocated before memtable flush. The same storage feeds
+    // both the base row LSN column and row-binlog LSN column.
+    std::shared_ptr<segment_v2::SegmentAllocatedLsnMap> allocated_lsn_map = 
nullptr;
+    bool _need_allocate_lsn = false;
+
+    void insert_segment_allocated_lsns(int64_t segment_id,
+                                       ConstAllocatedLsnVectorSharedPtr 
allocated_lsns) {
+        DCHECK(allocated_lsn_map != nullptr);
+        allocated_lsn_map->insert_segment_allocated_lsns(segment_id, 
std::move(allocated_lsns));
+    }
+
+    void remove_segment_allocated_lsns(int64_t segment_id) {
+        DCHECK(allocated_lsn_map != nullptr);
+        allocated_lsn_map->remove_segment(segment_id);
+    }
+
+    ConstAllocatedLsnVectorSharedPtr get_segment_allocated_lsns(int64_t 
segment_id) const {
+        DCHECK(allocated_lsn_map != nullptr);
+        return allocated_lsn_map->get_segment_allocated_lsns(segment_id);
+    }
+
+    bool need_allocated_lsn() const { return _need_allocate_lsn; }
+
     bool is_local_rowset() const { return !storage_resource; }
 
     std::string segment_path(int seg_id) const {
diff --git a/be/src/storage/rowset_builder.cpp 
b/be/src/storage/rowset_builder.cpp
index 5c825c3806c..f8fb243494f 100644
--- a/be/src/storage/rowset_builder.cpp
+++ b/be/src/storage/rowset_builder.cpp
@@ -530,6 +530,7 @@ Status GroupRowsetBuilder::init() {
     
RETURN_IF_ERROR(RowsetFactory::create_empty_group_rowset_writer(&group_writer));
     group_writer->set_data_writer(_txn_rs_builder->rowset_writer());
     
group_writer->set_row_binlog_writer(_row_binlog_rowset_builder->rowset_writer());
+    
RETURN_IF_ERROR(group_writer->init(_txn_rs_builder->rowset_writer()->context()));
 
     {
         const auto& data_ctx = _txn_rs_builder->rowset_writer()->context();
diff --git a/be/src/storage/tablet/base_tablet.cpp 
b/be/src/storage/tablet/base_tablet.cpp
index 06232a389fd..c4a487f80ce 100644
--- a/be/src/storage/tablet/base_tablet.cpp
+++ b/be/src/storage/tablet/base_tablet.cpp
@@ -890,8 +890,7 @@ Status 
BaseTablet::calc_segment_delete_bitmap(RowsetSharedPtr rowset,
                 for (auto p : sort_perm) {
                     lsn_ids->emplace_back(src.get_data()[p]);
                 }
-                
binlog_ctx.write_binlog_opt().write_binlog_config().insert_seg_lsn(
-                        segment_id, std::move(lsn_ids));
+                binlog_ctx.insert_segment_allocated_lsns(segment_id, 
std::move(lsn_ids));
             }
         }
         RETURN_IF_ERROR(rowset_writer->flush_single_block(&ordered_block, 
segment_id));
@@ -1677,6 +1676,7 @@ Status BaseTablet::update_delete_bitmap(const 
BaseTabletSPtr& self, TabletTxnInf
         
RETURN_IF_ERROR(RowsetFactory::create_empty_group_rowset_writer(&group_writer));
         group_writer->set_data_writer(data_writer_sp);
         group_writer->set_row_binlog_writer(row_binlog_writer_sp);
+        
RETURN_IF_ERROR(group_writer->init(group_writer->data_writer()->context()));
         transient_rs_writer = std::move(group_writer);
     }
 
diff --git a/be/src/storage/tablet/tablet_meta.cpp 
b/be/src/storage/tablet/tablet_meta.cpp
index acaae738d82..21ab367809f 100644
--- a/be/src/storage/tablet/tablet_meta.cpp
+++ b/be/src/storage/tablet/tablet_meta.cpp
@@ -395,6 +395,9 @@ void TabletMeta::init_schema_from_thrift(const 
TTabletSchema& tablet_schema,
     
tablet_schema_pb->set_num_short_key_columns(tablet_schema.short_key_column_count);
     
tablet_schema_pb->set_num_rows_per_row_block(config::default_num_rows_per_column_file_block);
     tablet_schema_pb->set_sequence_col_idx(tablet_schema.sequence_col_idx);
+    if (tablet_schema.__isset.row_lsn_col_idx) {
+        tablet_schema_pb->set_row_lsn_col_idx(tablet_schema.row_lsn_col_idx);
+    }
     if (tablet_schema.__isset.binlog_tso_idx) {
         tablet_schema_pb->set_binlog_tso_col_idx(tablet_schema.binlog_tso_idx);
     }
@@ -587,6 +590,9 @@ void TabletMeta::init_schema_from_thrift(const 
TTabletSchema& tablet_schema,
     if (tablet_schema.__isset.commit_tso_col_idx) {
         
tablet_schema_pb->set_commit_tso_col_idx(tablet_schema.commit_tso_col_idx);
     }
+    if (tablet_schema.__isset.row_lsn_col_idx) {
+        tablet_schema_pb->set_row_lsn_col_idx(tablet_schema.row_lsn_col_idx);
+    }
     if (tablet_schema.__isset.store_row_column) {
         tablet_schema_pb->set_store_row_column(tablet_schema.store_row_column);
     }
diff --git a/be/src/storage/tablet/tablet_schema.cpp 
b/be/src/storage/tablet/tablet_schema.cpp
index 1ac5586aaa8..9e612842a16 100644
--- a/be/src/storage/tablet/tablet_schema.cpp
+++ b/be/src/storage/tablet/tablet_schema.cpp
@@ -874,6 +874,8 @@ void TabletSchema::append_column(TabletColumn column, 
ColumnType col_type) {
         _skip_bitmap_col_idx = _num_columns;
     } else if (UNLIKELY(column.name() == COMMIT_TSO_COL)) {
         _commit_tso_col_idx = _num_columns;
+    } else if (UNLIKELY(column.name() == ROW_LSN_COL)) {
+        _row_lsn_col_idx = _num_columns;
     } else if (UNLIKELY(column.name() == BINLOG_TSO_COL)) {
         _binlog_tso_col_idx = _num_columns;
     } else if (UNLIKELY(column.name() == BINLOG_LSN_COL)) {
@@ -1083,6 +1085,7 @@ void TabletSchema::init_from_pb(const TabletSchemaPB& 
schema, bool ignore_extrac
     _version_col_idx = schema.version_col_idx();
     _skip_bitmap_col_idx = schema.skip_bitmap_col_idx();
     _commit_tso_col_idx = schema.commit_tso_col_idx();
+    _row_lsn_col_idx = schema.row_lsn_col_idx();
     _binlog_tso_col_idx = schema.binlog_tso_col_idx();
     _binlog_lsn_col_idx = schema.binlog_lsn_col_idx();
     _binlog_op_col_idx = schema.binlog_op_col_idx();
@@ -1190,6 +1193,7 @@ void TabletSchema::shawdow_copy_without_columns(const 
TabletSchema& tablet_schem
     _version_col_idx = -1;
     _skip_bitmap_col_idx = -1;
     _commit_tso_col_idx = -1;
+    _row_lsn_col_idx = -1;
     _binlog_tso_col_idx = -1;
     _binlog_lsn_col_idx = -1;
     _binlog_op_col_idx = -1;
@@ -1258,6 +1262,7 @@ void TabletSchema::build_current_tablet_schema(int64_t 
index_id, int32_t version
     _version_col_idx = -1;
     _skip_bitmap_col_idx = -1;
     _commit_tso_col_idx = -1;
+    _row_lsn_col_idx = -1;
     _binlog_tso_col_idx = -1;
     _binlog_lsn_col_idx = -1;
     _binlog_op_col_idx = -1;
@@ -1288,6 +1293,8 @@ void TabletSchema::build_current_tablet_schema(int64_t 
index_id, int32_t version
             _skip_bitmap_col_idx = _num_columns;
         } else if (UNLIKELY(column->name() == COMMIT_TSO_COL)) {
             _commit_tso_col_idx = _num_columns;
+        } else if (UNLIKELY(column->name() == ROW_LSN_COL)) {
+            _row_lsn_col_idx = _num_columns;
         } else if (UNLIKELY(column->name() == BINLOG_TSO_COL)) {
             _binlog_tso_col_idx = _num_columns;
         } else if (UNLIKELY(column->name() == BINLOG_LSN_COL)) {
@@ -1418,6 +1425,7 @@ void TabletSchema::to_schema_pb(TabletSchemaPB* 
tablet_schema_pb) const {
     tablet_schema_pb->set_version_col_idx(_version_col_idx);
     tablet_schema_pb->set_skip_bitmap_col_idx(_skip_bitmap_col_idx);
     tablet_schema_pb->set_commit_tso_col_idx(_commit_tso_col_idx);
+    tablet_schema_pb->set_row_lsn_col_idx(_row_lsn_col_idx);
     tablet_schema_pb->set_binlog_tso_col_idx(_binlog_tso_col_idx);
     tablet_schema_pb->set_binlog_lsn_col_idx(_binlog_lsn_col_idx);
     tablet_schema_pb->set_binlog_op_col_idx(_binlog_op_col_idx);
@@ -1803,6 +1811,7 @@ bool operator==(const TabletSchema& a, const 
TabletSchema& b) {
     if (a._version_col_idx != b._version_col_idx) return false;
     if (a._skip_bitmap_col_idx != b._skip_bitmap_col_idx) return false;
     if (a._commit_tso_col_idx != b._commit_tso_col_idx) return false;
+    if (a._row_lsn_col_idx != b._row_lsn_col_idx) return false;
     if (a._binlog_tso_col_idx != b._binlog_tso_col_idx) return false;
     if (a._binlog_lsn_col_idx != b._binlog_lsn_col_idx) return false;
     if (a._binlog_op_col_idx != b._binlog_op_col_idx) return false;
diff --git a/be/src/storage/tablet/tablet_schema.h 
b/be/src/storage/tablet/tablet_schema.h
index 9d797d4e6be..ed7646263a7 100644
--- a/be/src/storage/tablet/tablet_schema.h
+++ b/be/src/storage/tablet/tablet_schema.h
@@ -522,6 +522,7 @@ public:
     int32_t skip_bitmap_col_idx() const { return _skip_bitmap_col_idx; }
     bool is_tso_enabled() const { return _commit_tso_col_idx != -1 || 
_binlog_tso_col_idx != -1; }
     int32_t commit_tso_col_idx() const { return _commit_tso_col_idx; }
+    int32_t row_lsn_col_idx() const { return _row_lsn_col_idx; }
     int32_t binlog_tso_col_idx() const { return _binlog_tso_col_idx; }
     int32_t binlog_lsn_col_idx() const { return _binlog_lsn_col_idx; }
     int32_t binlog_op_col_idx() const { return _binlog_op_col_idx; }
@@ -823,6 +824,7 @@ private:
     int32_t _version_col_idx = -1;
     int32_t _skip_bitmap_col_idx = -1;
     int32_t _commit_tso_col_idx = -1;
+    int32_t _row_lsn_col_idx = -1;
     int32_t _binlog_tso_col_idx = -1;
     int32_t _binlog_lsn_col_idx = -1;
     int32_t _binlog_op_col_idx = -1;
diff --git a/be/src/storage/transform/row_binlog_derive.cpp 
b/be/src/storage/transform/row_binlog_derive.cpp
index b06db536f5e..3d11f169459 100644
--- a/be/src/storage/transform/row_binlog_derive.cpp
+++ b/be/src/storage/transform/row_binlog_derive.cpp
@@ -191,8 +191,8 @@ Status resolve_binlog_context(TransformExecContext& ctx, 
const Block* block,
         return Status::InternalError<false>(
                 "binlog<row> blocks must be flushed through 
flush_single_block");
     }
-    c->lsn_ids = cfg.get_seg_lsn(ctx.segment_id);
-    cfg.remove_seg(ctx.segment_id);
+    c->lsn_ids = ctx.rowset_ctx->get_segment_allocated_lsns(ctx.segment_id);
+    ctx.rowset_ctx->remove_segment_allocated_lsns(ctx.segment_id);
     CHECK(c->lsn_ids->size() >= c->num_rows) << c->lsn_ids->size() << " vs " 
<< c->num_rows;
 
     // Preserve the source writer's layout: system columns may be a prefix or
diff --git a/be/src/storage/transform/row_binlog_derive.h 
b/be/src/storage/transform/row_binlog_derive.h
index 2d735cb9075..b7e174e1a74 100644
--- a/be/src/storage/transform/row_binlog_derive.h
+++ b/be/src/storage/transform/row_binlog_derive.h
@@ -17,6 +17,7 @@
 
 #pragma once
 
+#include "storage/rowset/rowset_fwd.h"
 #include "storage/transform/block_transform.h"
 
 namespace doris {
@@ -40,7 +41,7 @@ bool binlog_needs_historical_lookup(const 
RowsetWriterContext& context);
 struct BinlogDeriveContext {
     TabletSchemaSPtr binlog_schema;
     TabletSchemaSPtr source_schema;
-    std::shared_ptr<const std::vector<int64_t>> lsn_ids;
+    ConstAllocatedLsnVectorSharedPtr lsn_ids;
     size_t num_rows = 0;
     uint32_t binlog_tso_cid = 0;
     uint32_t binlog_lsn_cid = 0;
diff --git a/be/src/storage/utils.h b/be/src/storage/utils.h
index a6ea7a1874a..ee6f71fb6f8 100644
--- a/be/src/storage/utils.h
+++ b/be/src/storage/utils.h
@@ -40,6 +40,7 @@ static const std::string VERSION_COL = 
"__DORIS_VERSION_COL__";
 static const std::string SKIP_BITMAP_COL = "__DORIS_SKIP_BITMAP_COL__";
 static const std::string SEQUENCE_COL = "__DORIS_SEQUENCE_COL__";
 static const std::string COMMIT_TSO_COL = "__DORIS_COMMIT_TSO_COL__";
+static const std::string ROW_LSN_COL = "__DORIS_ROW_LSN_COL__";
 static const std::string BINLOG_TSO_COL = "__DORIS_BINLOG_TSO__";
 static const std::string BINLOG_LSN_COL = "__DORIS_BINLOG_LSN__";
 static const std::string BINLOG_OP_COL = "__DORIS_BINLOG_OP__";
diff --git a/be/test/load/memtable/memtable_flush_executor_test.cpp 
b/be/test/load/memtable/memtable_flush_executor_test.cpp
index 24bb7dd99e1..b53d0891f8c 100644
--- a/be/test/load/memtable/memtable_flush_executor_test.cpp
+++ b/be/test/load/memtable/memtable_flush_executor_test.cpp
@@ -65,6 +65,9 @@ public:
 
     Status init(const RowsetWriterContext& ctx) override {
         _context = ctx;
+        _context._need_allocate_lsn = _context.write_binlog_opt().enable ||
+                                      (_context.tablet_schema != nullptr &&
+                                       
_context.tablet_schema->row_lsn_col_idx() >= 0);
         return Status::OK();
     }
 
@@ -80,6 +83,12 @@ public:
             
std::this_thread::sleep_for(std::chrono::milliseconds(_flush_delay_ms));
         }
         _last_segment_id = segment_id;
+        // Capture LSN during flush, or it is released after flush.
+        if (_context.need_allocated_lsn()) {
+            _last_seg_lsn = _context.get_segment_allocated_lsns(segment_id);
+            EXPECT_NE(_last_seg_lsn, nullptr);
+            EXPECT_EQ(_last_seg_lsn->size(), block->rows());
+        }
         ++(*_flush_cnt);
         *flush_size = 1;
         if (_fail_on_flush) {
@@ -113,6 +122,8 @@ public:
 
     int32_t last_segment_id() const { return _last_segment_id; }
 
+    ConstAllocatedLsnVectorSharedPtr last_seg_lsn() const { return 
_last_seg_lsn; }
+
 private:
     std::atomic<int>* _flush_cnt;
     bool _fail_on_flush;
@@ -120,6 +131,7 @@ private:
     int _flush_delay_ms;
     int32_t _next_segment_id = 0;
     int32_t _last_segment_id = -1;
+    ConstAllocatedLsnVectorSharedPtr _last_seg_lsn = nullptr;
 };
 
 struct GroupFlushTestContext {
@@ -265,7 +277,7 @@ protected:
         TabletAddRowsPayload rows;
         for (size_t i = 0; i < lsns.size(); ++i) {
             rows.row_idxs.emplace_back(i);
-            rows.row_binlog_lsns.emplace_back(lsns[i]);
+            rows.allocated_lsns.emplace_back(lsns[i]);
         }
         ASSERT_TRUE(ctx->memtable->insert(&block, rows).ok());
     }
@@ -467,9 +479,7 @@ TEST_F(MemTableFlushExecutorGroupFlushTest, 
TestGroupFlushToken) {
         EXPECT_EQ(1, data_flush_cnt.load());
         EXPECT_EQ(1, binlog_flush_cnt.load());
         EXPECT_EQ(data_writer->last_segment_id(), 
binlog_writer->last_segment_id());
-        auto seg_lsn =
-                
binlog_writer->context().write_binlog_opt().write_binlog_config().get_seg_lsn(
-                        binlog_writer->last_segment_id());
+        auto seg_lsn = binlog_writer->last_seg_lsn();
         ASSERT_NE(seg_lsn, nullptr);
         ASSERT_EQ(ctx.memtable->raw_rows(), seg_lsn->size());
         EXPECT_EQ(1000, (*seg_lsn)[0]);
diff --git a/be/test/olap/rowset/cloud_group_rowset_builder_writer_test.cpp 
b/be/test/olap/rowset/cloud_group_rowset_builder_writer_test.cpp
index 7f337a5ded6..bc6572d1a4a 100644
--- a/be/test/olap/rowset/cloud_group_rowset_builder_writer_test.cpp
+++ b/be/test/olap/rowset/cloud_group_rowset_builder_writer_test.cpp
@@ -296,9 +296,10 @@ protected:
         cfg.source.source_write_type = DataWriteType::TYPE_DIRECT;
         auto lsn_buffer = AutoIncIDBuffer::create_shared(1, 1, 
kBinlogLsnAutoIncId);
         lsn_buffer->append_range_for_test(1000, num_rows);
-        auto lsn_ids = std::make_shared<std::vector<int64_t>>();
-        RETURN_IF_ERROR(allocate_binlog_lsn(lsn_buffer, num_rows, *lsn_ids));
-        cfg.insert_seg_lsn(0, lsn_ids);
+        std::vector<int64_t> allocated_lsns;
+        RETURN_IF_ERROR(allocate_lsn(lsn_buffer, num_rows, allocated_lsns));
+        auto lsn_ids = 
std::make_shared<std::vector<int64_t>>(allocated_lsns.begin(),
+                                                              
allocated_lsns.end());
 
         auto row_binlog_writer_res =
                 _row_binlog_tablet->create_rowset_writer(row_binlog_context, 
false);
@@ -313,6 +314,10 @@ protected:
         (*group_writer)
                 ->set_row_binlog_writer(
                         
std::shared_ptr<RowsetWriter>(std::move(row_binlog_writer_res.value())));
+        
RETURN_IF_ERROR((*group_writer)->init((*group_writer)->data_writer()->context()));
+        auto& group_binlog_ctx =
+                
const_cast<RowsetWriterContext&>((*group_writer)->row_binlog_writer()->context());
+        group_binlog_ctx.insert_segment_allocated_lsns(0, lsn_ids);
         return Status::OK();
     }
 
diff --git a/be/test/olap/rowset/group_rowset_writer_test.cpp 
b/be/test/olap/rowset/group_rowset_writer_test.cpp
index 4047dc53b22..604c8366c41 100644
--- a/be/test/olap/rowset/group_rowset_writer_test.cpp
+++ b/be/test/olap/rowset/group_rowset_writer_test.cpp
@@ -187,8 +187,10 @@ protected:
         auto lsn_buffer = AutoIncIDBuffer::create_shared(1, 1, 
kBinlogLsnAutoIncId);
         lsn_buffer->append_range_for_test(1000, num_rows);
         auto lsn_ids = std::make_shared<std::vector<int64_t>>();
-        RETURN_IF_ERROR_RESULT(allocate_binlog_lsn(lsn_buffer, num_rows, 
*lsn_ids));
-        binlog_options.insert_seg_lsn(0, lsn_ids);
+        RETURN_IF_ERROR_RESULT(allocate_lsn(lsn_buffer, num_rows, *lsn_ids));
+        row_binlog_context.allocated_lsn_map =
+                std::make_shared<segment_v2::SegmentAllocatedLsnMap>();
+        row_binlog_context.insert_segment_allocated_lsns(0, lsn_ids);
         return _row_binlog_tablet->create_rowset_writer(row_binlog_context, 
false);
     }
 
@@ -220,6 +222,11 @@ protected:
                 
std::shared_ptr<RowsetWriter>(std::move(data_writer_result.value())));
         group_writer->set_row_binlog_writer(
                 
std::shared_ptr<RowsetWriter>(std::move(row_binlog_writer_result.value())));
+        auto& row_binlog_context =
+                
const_cast<RowsetWriterContext&>(group_writer->row_binlog_writer()->context());
+        auto lsn_ids = row_binlog_context.get_segment_allocated_lsns(0);
+        
RETURN_IF_ERROR_RESULT(group_writer->init(group_writer->data_writer()->context()));
+        row_binlog_context.insert_segment_allocated_lsns(0, lsn_ids);
         return group_writer;
     }
 
@@ -256,9 +263,10 @@ protected:
         cfg.source.source_write_type = DataWriteType::TYPE_DIRECT;
         auto lsn_buffer = AutoIncIDBuffer::create_shared(1, 1, 
kBinlogLsnAutoIncId);
         lsn_buffer->append_range_for_test(1000, num_rows);
-        auto lsn_ids = std::make_shared<std::vector<int64_t>>();
-        RETURN_IF_ERROR(allocate_binlog_lsn(lsn_buffer, num_rows, *lsn_ids));
-        cfg.insert_seg_lsn(0, lsn_ids);
+        std::vector<int64_t> allocated_lsns;
+        RETURN_IF_ERROR(allocate_lsn(lsn_buffer, num_rows, allocated_lsns));
+        auto lsn_ids = 
std::make_shared<std::vector<int64_t>>(allocated_lsns.begin(),
+                                                              
allocated_lsns.end());
         auto row_binlog_writer_res =
                 _row_binlog_tablet->create_rowset_writer(row_binlog_context, 
false);
         if (!row_binlog_writer_res.has_value()) {
@@ -272,6 +280,10 @@ protected:
         (*group_writer)
                 ->set_row_binlog_writer(
                         
std::shared_ptr<RowsetWriter>(std::move(row_binlog_writer_res.value())));
+        
RETURN_IF_ERROR((*group_writer)->init((*group_writer)->data_writer()->context()));
+        auto& group_binlog_ctx =
+                
const_cast<RowsetWriterContext&>((*group_writer)->row_binlog_writer()->context());
+        group_binlog_ctx.insert_segment_allocated_lsns(0, lsn_ids);
         return Status::OK();
     }
 
diff --git a/be/test/storage/mow/mow_transform_test_base.h 
b/be/test/storage/mow/mow_transform_test_base.h
index 3dccf20ffe1..803f13f838c 100644
--- a/be/test/storage/mow/mow_transform_test_base.h
+++ b/be/test/storage/mow/mow_transform_test_base.h
@@ -475,8 +475,7 @@ protected:
     }
 
     // The per-segment LSN range the binlog derive consumes (one LSN per row).
-    static std::shared_ptr<std::vector<int64_t>> make_seg_lsn(size_t num_rows,
-                                                              int64_t start = 
1000) {
+    static AllocatedLsnVectorSharedPtr make_seg_lsn(size_t num_rows, int64_t 
start = 1000) {
         auto lsn_ids = std::make_shared<std::vector<int64_t>>();
         for (size_t i = 0; i < num_rows; ++i) {
             lsn_ids->push_back(start + static_cast<int64_t>(i));
diff --git a/be/test/storage/rowset/segment_flusher_format_test.cpp 
b/be/test/storage/rowset/segment_flusher_format_test.cpp
index 92ff3bdfb7b..a29164599cc 100644
--- a/be/test/storage/rowset/segment_flusher_format_test.cpp
+++ b/be/test/storage/rowset/segment_flusher_format_test.cpp
@@ -3104,6 +3104,7 @@ protected:
             source_mow_context = make_mow_context(source_tablet->tablet_id(), 
history);
         }
         context.write_binlog_opt().enable = true;
+        context.allocated_lsn_map = 
std::make_shared<segment_v2::SegmentAllocatedLsnMap>();
         context.write_binlog_opt().set_need_before(need_before);
         auto& options = context.write_binlog_opt().write_binlog_config();
         options.source.tablet_schema = source_tablet->tablet_schema();
@@ -3117,7 +3118,7 @@ protected:
             for (int64_t row = 0; row < 3; ++row) {
                 lsn_ids->push_back(1000 + segment_id * 100 + row);
             }
-            options.insert_seg_lsn(segment_id, std::move(lsn_ids));
+            context.insert_segment_allocated_lsns(segment_id, 
std::move(lsn_ids));
         }
     }
 
diff --git a/be/test/storage/transform/row_binlog_derive_test.cpp 
b/be/test/storage/transform/row_binlog_derive_test.cpp
index b9df3aec1d5..4ea6bb7ab17 100644
--- a/be/test/storage/transform/row_binlog_derive_test.cpp
+++ b/be/test/storage/transform/row_binlog_derive_test.cpp
@@ -308,6 +308,11 @@ protected:
         ctx.segment_id = 0;
         return ctx;
     }
+
+    void register_segment_lsns(RowsetWriterContext& rwc, size_t num_rows) {
+        rwc.allocated_lsn_map = 
std::make_shared<segment_v2::SegmentAllocatedLsnMap>();
+        rwc.insert_segment_allocated_lsns(0, make_seg_lsn(num_rows));
+    }
 };
 
 // ===========================================================================
@@ -334,7 +339,7 @@ TEST_F(RowBinlogDeriveTest, 
PlainAppendAndDeleteWithAfterAndLsn) {
     cfg.source.source_write_type = DataWriteType::TYPE_DIRECT;
     cfg.source.is_transient_rowset_writer = false;
     cfg.write_before = false; // no PU, no BEFORE -> plain derive
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     auto chain = build_transform_chain(rwc);
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"PlainRowBinlogDerive"}));
@@ -400,7 +405,7 @@ TEST_F(RowBinlogDeriveTest, 
PlainNoDeleteSignColumnAllAppend) {
     cfg.source.source_write_type = DataWriteType::TYPE_DIRECT;
     cfg.source.is_transient_rowset_writer = false;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     auto chain = build_transform_chain(rwc);
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"PlainRowBinlogDerive"}));
@@ -491,7 +496,7 @@ TEST_F(RowBinlogDeriveTest, 
PlainMapsHiddenKeysAndSkipsHiddenNonKeys) {
     cfg.source.tablet_schema = source_schema;
     cfg.source.source_write_type = DataWriteType::TYPE_DIRECT;
     cfg.source.is_transient_rowset_writer = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     Block block = source_schema->create_storage_block();
     {
@@ -578,7 +583,7 @@ TEST_F(RowBinlogDeriveTest, 
MowUpdateAndAppendTakesHistoryV2) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     auto chain = build_transform_chain(rwc);
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"MowRowBinlogDerive"}));
@@ -722,7 +727,7 @@ TEST_F(RowBinlogDeriveTest, 
MowHiddenKeyColumnIsPartOfTheProbeKey) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(1));
+    register_segment_lsns(rwc, 1);
 
     auto chain = build_transform_chain(rwc);
     TransformExecContext ctx = exec_ctx(binlog_schema, &rwc);
@@ -800,7 +805,7 @@ TEST_F(RowBinlogDeriveTest, 
MowPartialUpdateWithBeforeImage) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = true;
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     auto chain = build_transform_chain(rwc);
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"MowRowBinlogDerive"}));
@@ -893,7 +898,7 @@ TEST_F(RowBinlogDeriveTest, 
MowInsertAfterDeletedHistoryRowIsAppend) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     auto chain = build_transform_chain(rwc);
     TransformExecContext ctx = exec_ctx(binlog_schema, &rwc);
@@ -979,7 +984,7 @@ TEST_F(RowBinlogDeriveTest, 
MowLooksUpHistoryInBaseTabletNotBinlogTablet) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(1));
+    register_segment_lsns(rwc, 1);
 
     auto chain = build_transform_chain(rwc);
     TransformExecContext ctx = exec_ctx(binlog_schema, &rwc);
@@ -1053,7 +1058,7 @@ TEST_F(RowBinlogDeriveTest, MowDeleteExistingAndNewKey) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     auto chain = build_transform_chain(rwc);
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"MowRowBinlogDerive"}));
@@ -1136,7 +1141,7 @@ TEST_F(RowBinlogDeriveTest, MowBeforeImageMirrorsHistory) 
{
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = true;
-    cfg.insert_seg_lsn(0, make_seg_lsn(3));
+    register_segment_lsns(rwc, 3);
 
     // setup_retriever_and_lookup requires partial_update_info on the retriever
     // context (build_after_block / retrieve_historical_row DCHECK it). For a
@@ -1258,7 +1263,7 @@ TEST_F(RowBinlogDeriveTest, 
MowBeforeNoopZeroValueColumns) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = true;
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     auto chain = build_transform_chain(rwc);
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"MowRowBinlogDerive"}));
@@ -1346,7 +1351,7 @@ TEST_F(RowBinlogDeriveTest, 
MowSeqSourceSeqLosesStillReadsHistory) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     auto chain = build_transform_chain(rwc);
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"MowRowBinlogDerive"}));
@@ -1420,7 +1425,7 @@ TEST_F(RowBinlogDeriveTest, 
MowRejectsFlexiblePartialUpdate) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = mow;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(2));
+    register_segment_lsns(rwc, 2);
 
     auto chain = build_transform_chain(rwc);
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"MowRowBinlogDerive"}));
@@ -1490,7 +1495,7 @@ TEST_F(RowBinlogDeriveTest, 
MowFixedPartialUpdateRejectsBadWidth) {
 
     // too narrow: no columns (0 < num_key_columns 1)
     {
-        cfg.insert_seg_lsn(0, make_seg_lsn(1));
+        register_segment_lsns(rwc, 1);
         TransformExecContext ctx = make_ctx();
         Block block;
         auto st = chain.apply(ctx, &block);
@@ -1500,7 +1505,7 @@ TEST_F(RowBinlogDeriveTest, 
MowFixedPartialUpdateRejectsBadWidth) {
     }
     // too wide: full width including hidden delete_sign (4 >= num_columns 4)
     {
-        cfg.insert_seg_lsn(0, make_seg_lsn(1));
+        register_segment_lsns(rwc, 1);
         TransformExecContext ctx = make_ctx();
         Block block = source_schema->create_storage_block(); // 4 columns
         block.get_by_position(0).column->assert_mutable()->insert_default();
@@ -1531,7 +1536,7 @@ TEST_F(RowBinlogDeriveTest, RejectsMissingSourceSchema) {
     cfg.source.source_write_type = DataWriteType::TYPE_DIRECT;
     cfg.source.is_transient_rowset_writer = false;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(1));
+    register_segment_lsns(rwc, 1);
 
     auto chain = build_transform_chain(rwc); // no PU, no BEFORE -> plain 
derive
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"PlainRowBinlogDerive"}));
@@ -1568,7 +1573,7 @@ TEST_F(RowBinlogDeriveTest, RejectsNegativeSegmentId) {
     cfg.source.source_write_type = DataWriteType::TYPE_DIRECT;
     cfg.source.is_transient_rowset_writer = false;
     cfg.write_before = false;
-    cfg.insert_seg_lsn(0, make_seg_lsn(1));
+    register_segment_lsns(rwc, 1);
 
     auto chain = build_transform_chain(rwc);
     EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> 
{"PlainRowBinlogDerive"}));
@@ -1607,7 +1612,7 @@ TEST_F(RowBinlogDeriveTest, 
RejectsSchemaAndWriteBeforeDisagreement) {
     cfg.source.is_transient_rowset_writer = false;
     cfg.source.mow_context = make_mow_context(100, {});
     cfg.write_before = true; // the schema has no BEFORE columns
-    cfg.insert_seg_lsn(0, make_seg_lsn(1));
+    register_segment_lsns(rwc, 1);
 
     auto chain = build_transform_chain(rwc);
     TransformExecContext ctx = exec_ctx(binlog_schema, &rwc);
diff --git a/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Column.java 
b/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Column.java
index 91289f09e17..7ce5d2a3f9d 100644
--- a/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Column.java
+++ b/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Column.java
@@ -62,9 +62,11 @@ public class Column implements GsonPostProcessable {
     public static final String ICEBERG_ROWID_COL = 
"__DORIS_ICEBERG_ROWID_COL__";
     // For time-travel (FOR VERSION/TIME AS OF) on duplicate / mow tables with 
row binlog enabled.
     public static final String COMMIT_TSO_COL = "__DORIS_COMMIT_TSO_COL__";
+    public static final String ROW_LSN_COL = "__DORIS_ROW_LSN_COL__";
     // table stream columns
     public static final String STREAM_CHANGE_TYPE_COL = 
"__DORIS_STREAM_CHANGE_TYPE_COL__";
     public static final String STREAM_SEQ_COL = 
"__DORIS_STREAM_SEQUENCE_COL__";
+    public static final String STREAM_LSN_COL = "__DORIS_STREAM_LSN_COL__";
     // NOTE: you should name hidden column start with '__DORIS_' 
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 
     private static final String COLUMN_ARRAY_CHILDREN = "item";
@@ -75,6 +77,8 @@ public class Column implements GsonPostProcessable {
     private static final String COLUMN_MAP_VALUE = "value";
     public static final Column STREAM_SEQ_VIRTUAL_COLUMN =
             new Column(STREAM_SEQ_COL, Type.BIGINT, false, null, true, null, 
false);
+    public static final Column STREAM_LSN_VIRTUAL_COLUMN =
+            new Column(STREAM_LSN_COL, Type.BIGINT, false, null, true, null, 
false);
     public static final Column STREAM_CHANGE_TYPE_VIRTUAL_COLUMN =
             new Column(STREAM_CHANGE_TYPE_COL, Type.STRING, false, null, true, 
null, false);
 
@@ -531,6 +535,10 @@ public class Column implements GsonPostProcessable {
         return !visible && aggregationType == AggregateType.NONE && 
nameEquals(COMMIT_TSO_COL, true);
     }
 
+    public boolean isRowLsnColumn() {
+        return !visible && aggregationType == AggregateType.NONE && 
nameEquals(ROW_LSN_COL, true);
+    }
+
     // now we only support BloomFilter on (same behavior with BE):
     // smallint/int/bigint/largeint
     // string/varchar/char/variant
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
index 0aee3ea4f8e..eec6e523ad1 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
@@ -2389,6 +2389,11 @@ public class OlapTable extends Table implements 
MTMVRelatedTableIf, GsonPostProc
         return getBinlogConfig().isEnableForStreaming();
     }
 
+    // Whether the base table physically stores the row LSN column (dup table 
with row binlog).
+    public boolean hasRowLsnColumn() {
+        return getBaseSchema(true).stream().anyMatch(Column::isRowLsnColumn);
+    }
+
     public void createNewRowBinlogMeta(IdGeneratorBuffer idGeneratorBuffer, 
long dbId)
             throws DdlException {
         writeLock();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/stream/TableStreamBuildFactory.java
 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/stream/TableStreamBuildFactory.java
index 105015626df..619b1e60a9f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/stream/TableStreamBuildFactory.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/stream/TableStreamBuildFactory.java
@@ -19,6 +19,7 @@ package org.apache.doris.catalog.stream;
 
 
 import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.OlapTable;
 import org.apache.doris.catalog.TableIf;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.DdlException;
@@ -61,6 +62,13 @@ public class TableStreamBuildFactory {
         Column sequenceColumn = new Column(Column.STREAM_SEQ_COL, Type.BIGINT);
         sequenceColumn.setIsVisible(false);
         schema.add(sequenceColumn);
+        // Only expose stream LSN when the base table stores row LSN, e.g. dup 
table with binlog.
+        if (params.baseTable instanceof OlapTable
+                && ((OlapTable) params.baseTable).hasRowLsnColumn()) {
+            Column lsnColumn = new Column(Column.STREAM_LSN_COL, Type.BIGINT);
+            lsnColumn.setIsVisible(false);
+            schema.add(lsnColumn);
+        }
         Column changeTypeColumn = new Column(Column.STREAM_CHANGE_TYPE_COL, 
Type.VARCHAR);
         changeTypeColumn.setIsVisible(false);
         schema.add(changeTypeColumn);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/datasource/CloudInternalCatalog.java
 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/datasource/CloudInternalCatalog.java
index 5c604c94677..59dd30ca988 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/datasource/CloudInternalCatalog.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/datasource/CloudInternalCatalog.java
@@ -508,6 +508,7 @@ public class CloudInternalCatalog extends InternalCatalog {
         int deleteSign = -1;
         int sequenceCol = -1;
         int commitTsoCol = -1;
+        int rowLsnCol = -1;
         for (int i = 0; i < schemaColumns.size(); i++) {
             Column column = schemaColumns.get(i);
             if (column.isDeleteSignColumn()) {
@@ -519,10 +520,14 @@ public class CloudInternalCatalog extends InternalCatalog 
{
             if (column.isCommitTsoColumn()) {
                 commitTsoCol = i;
             }
+            if (column.isRowLsnColumn()) {
+                rowLsnCol = i;
+            }
         }
         schemaBuilder.setDeleteSignIdx(deleteSign);
         schemaBuilder.setSequenceColIdx(sequenceCol);
         schemaBuilder.setCommitTsoColIdx(commitTsoCol);
+        schemaBuilder.setRowLsnColIdx(rowLsnCol);
         schemaBuilder.setStoreRowColumn(storeRowColumn);
 
         if (dataSortInfo.getSortType() == TSortType.LEXICAL) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
index 65a426da75f..b921b5ec970 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
@@ -2404,7 +2404,7 @@ public class InternalCatalog implements 
CatalogIf<Database> {
             throw new DdlException("Cannot create temporary table with binlog 
enable");
         }
         
createTableInfo.getProperties().putAll(createTableBinlogConfig.toProperties());
-        
createTableInfo.createCommitTSOColumnIfNecessary(createTableBinlogConfig);
+        
createTableInfo.createRowBinlogHiddenColumnsIfNecessary(createTableBinlogConfig);
 
         // get keys type
         KeysDesc keysDesc = createTableInfo.getKeysDesc();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NormalizeOlapTableStreamScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NormalizeOlapTableStreamScan.java
index 2a2d0e9b848..7bebf5b0d78 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NormalizeOlapTableStreamScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NormalizeOlapTableStreamScan.java
@@ -69,8 +69,8 @@ import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
- * 1. remove STREAM_CHANGE_TYPE_VIRTUAL_COLUMN & STREAM_SEQ_VIRTUAL_COLUMN 
from olap table stream scan output
- * with alias projection
+ * 1. remove STREAM_CHANGE_TYPE_VIRTUAL_COLUMN / STREAM_SEQ_VIRTUAL_COLUMN / 
STREAM_LSN_VIRTUAL_COLUMN
+ * from olap table stream scan output with alias projection
  * 2. add delete sign column if unique base table
  */
 public class NormalizeOlapTableStreamScan extends OneRewriteRuleFactory {
@@ -93,6 +93,12 @@ public class NormalizeOlapTableStreamScan extends 
OneRewriteRuleFactory {
                         new VarcharLiteral("UPDATE_AFTER"))), new 
VarcharLiteral("UNKNOWN"));
     }
 
+    private static boolean isStreamVirtualSlot(Slot slot, Column 
virtualColumn) {
+        return slot instanceof SlotReference
+                && ((SlotReference) slot).getOriginalColumn().isPresent()
+                && ((SlotReference) 
slot).getOriginalColumn().get().equals(virtualColumn);
+    }
+
     private Plan normalize(LogicalOlapTableStreamScan scan, CascadesContext 
cascadesContext) {
         // short-cut for empty partition
         if (scan.getSelectedPartitionIds().isEmpty()) {
@@ -152,8 +158,8 @@ public class NormalizeOlapTableStreamScan extends 
OneRewriteRuleFactory {
      *
      * <p>{@code isIncremental} distinguishes the two callers:
      * <ul>
-     *   <li>true  — normal stream consumption: map the binlog op/timestamp 
columns into the stream
-     *               virtual columns STREAM_CHANGE_TYPE_COL / 
STREAM_SEQ_COL.</li>
+     *   <li>true  — normal stream consumption: map the binlog 
op/timestamp/lsn columns into the
+     *               stream virtual columns STREAM_CHANGE_TYPE_COL / 
STREAM_SEQ_COL / STREAM_LSN_COL.</li>
      *   <li>false — snapshot rebuild: only keep DELETE &amp; UPDATE_BEFORE 
rows so they can be added
      *               back to reconstruct the "before" image at the snapshot 
point.</li>
      * </ul>
@@ -180,9 +186,12 @@ public class NormalizeOlapTableStreamScan extends 
OneRewriteRuleFactory {
         // project stream virtual slot from binlog
         Slot opSlot = null;
         Slot seqSlot = null;
+        Slot lsnSlot = null;
         for (int i = 0; i < binlogOutputSlots.size(); i++) {
             if 
(binlogOutputSlots.get(i).getName().equals(Column.BINLOG_TSO_COL)) {
                 seqSlot = binlogOutputSlots.get(i);
+            } else if 
(binlogOutputSlots.get(i).getName().equals(Column.BINLOG_LSN_COL)) {
+                lsnSlot = binlogOutputSlots.get(i);
             } else if 
(binlogOutputSlots.get(i).getName().equals(Column.BINLOG_OPERATION_COL)) {
                 opSlot = binlogOutputSlots.get(i);
             }
@@ -197,17 +206,15 @@ public class NormalizeOlapTableStreamScan extends 
OneRewriteRuleFactory {
         if (isIncremental) {
             // replace stream virtual column with alias slot reference
             for (Slot slot : originSlots) {
-                if (slot instanceof SlotReference
-                        && ((SlotReference) 
slot).getOriginalColumn().isPresent()
-                        && ((SlotReference) slot).getOriginalColumn().get()
-                        .equals(Column.STREAM_CHANGE_TYPE_VIRTUAL_COLUMN)) {
+                if (isStreamVirtualSlot(slot, 
Column.STREAM_CHANGE_TYPE_VIRTUAL_COLUMN)) {
                     project.add(new 
Alias(StatementScopeIdGenerator.newExprId(), buildChangeTypeExpr(opSlot),
                             Column.STREAM_CHANGE_TYPE_COL));
-                } else if (slot instanceof SlotReference
-                        && ((SlotReference) 
slot).getOriginalColumn().isPresent()
-                        && ((SlotReference) slot).getOriginalColumn().get()
-                        .equals(Column.STREAM_SEQ_VIRTUAL_COLUMN)) {
+                } else if (isStreamVirtualSlot(slot, 
Column.STREAM_SEQ_VIRTUAL_COLUMN)) {
+                    Preconditions.checkArgument(seqSlot != null, "Commit tso 
column not found in binlog output");
                     project.add(new 
Alias(StatementScopeIdGenerator.newExprId(), seqSlot, Column.STREAM_SEQ_COL));
+                } else if (isStreamVirtualSlot(slot, 
Column.STREAM_LSN_VIRTUAL_COLUMN)) {
+                    Preconditions.checkArgument(lsnSlot != null, "Row lsn 
column not found in binlog output");
+                    project.add(new 
Alias(StatementScopeIdGenerator.newExprId(), lsnSlot, Column.STREAM_LSN_COL));
                 }
             }
         } else {
@@ -337,12 +344,12 @@ public class NormalizeOlapTableStreamScan extends 
OneRewriteRuleFactory {
      * <p>Selected partitions are split into:
      * <ul>
      *   <li>historical partitions — never consumed history data; scanned from 
the base table and all
-     *       rows are treated as APPEND (change type = "APPEND", seq = commit 
tso);</li>
+     *       rows are treated as APPEND (change type = "APPEND", seq = commit 
tso, lsn = row lsn);</li>
      *   <li>incremental partitions — read row-level changes from binlog via
      *       {@link #makeIncrementalScanFromBinlog}.</li>
      * </ul>
-     * The two plans are unioned. {@code notVirtualSlots} are the origin 
output slots excluding the
-     * two stream virtual columns (STREAM_CHANGE_TYPE / STREAM_SEQ), which are 
filled separately.
+     * The two plans are unioned. {@code notVirtualSlots} are the origin 
output slots excluding
+     * stream virtual columns (STREAM_CHANGE_TYPE / STREAM_SEQ / STREAM_LSN), 
which are filled separately.
      */
     private Plan makeTableStreamScan(LogicalOlapTableStreamScan scan, 
CascadesContext cascadesContext) {
         OlapTableStreamWrapper streamWrapper = scan.getTable();
@@ -353,16 +360,11 @@ public class NormalizeOlapTableStreamScan extends 
OneRewriteRuleFactory {
         Plan historyPlan = null;
         Plan incrementalPlan = null;
         List<Slot> originSlots = scan.getOutput();
-        // notVirtualSlots = originSlots - (STREAM_CHANGE_TYPE_VIRTUAL_COLUMN 
+ STREAM_SEQ_VIRTUAL_COLUMN)
+        // notVirtualSlots = originSlots - stream virtual columns
         List<Slot> notVirtualSlots = originSlots.stream()
-                .filter(slot -> !(slot instanceof SlotReference
-                        && ((SlotReference) 
slot).getOriginalColumn().isPresent()
-                        && ((SlotReference) slot).getOriginalColumn().get()
-                        .equals(Column.STREAM_CHANGE_TYPE_VIRTUAL_COLUMN)))
-                .filter(slot -> !(slot instanceof SlotReference
-                        && ((SlotReference) 
slot).getOriginalColumn().isPresent()
-                        && ((SlotReference) slot).getOriginalColumn().get()
-                        .equals(Column.STREAM_SEQ_VIRTUAL_COLUMN)))
+                .filter(slot -> !isStreamVirtualSlot(slot, 
Column.STREAM_CHANGE_TYPE_VIRTUAL_COLUMN))
+                .filter(slot -> !isStreamVirtualSlot(slot, 
Column.STREAM_SEQ_VIRTUAL_COLUMN))
+                .filter(slot -> !isStreamVirtualSlot(slot, 
Column.STREAM_LSN_VIRTUAL_COLUMN))
                 .collect(ImmutableList.toImmutableList());
 
         // history plan
@@ -371,30 +373,33 @@ public class NormalizeOlapTableStreamScan extends 
OneRewriteRuleFactory {
             Plan plan = makeOlapScanOnBaseTable(scan, cascadesContext, 
baseTable, historicalPartitionIds);
             List<Slot> baseOutputSlots = plan.getOutput();
             Slot tsoSlot = null;
+            Slot lsnSlot = null;
             for (Slot slot : baseOutputSlots) {
                 if (slot.getName().equals(Column.COMMIT_TSO_COL)) {
                     tsoSlot = slot;
+                } else if (slot.getName().equals(Column.ROW_LSN_COL)) {
+                    lsnSlot = slot;
                 }
-                if (tsoSlot != null) {
+                if (tsoSlot != null && lsnSlot != null) {
                     break;
                 }
             }
             Preconditions.checkArgument(tsoSlot != null, "Commit tso column 
not found in base table output");
+            if (baseTable.hasRowLsnColumn()) {
+                Preconditions.checkArgument(lsnSlot != null, "Row lsn column 
not found in base table output");
+            }
             List<NamedExpression> project = 
mapOriginOutputFromChild(notVirtualSlots, baseOutputSlots, false);
             for (Slot slot : originSlots) {
-                if (slot instanceof SlotReference
-                        && ((SlotReference) 
slot).getOriginalColumn().isPresent()
-                        && ((SlotReference) slot).getOriginalColumn().get()
-                        .equals(Column.STREAM_CHANGE_TYPE_VIRTUAL_COLUMN)) {
+                if (isStreamVirtualSlot(slot, 
Column.STREAM_CHANGE_TYPE_VIRTUAL_COLUMN)) {
                     project.add(new 
Alias(StatementScopeIdGenerator.newExprId(), new VarcharLiteral("APPEND"),
                             Column.STREAM_CHANGE_TYPE_COL));
                 }
-                if (slot instanceof SlotReference
-                        && ((SlotReference) 
slot).getOriginalColumn().isPresent()
-                        && ((SlotReference) slot).getOriginalColumn().get()
-                        .equals(Column.STREAM_SEQ_VIRTUAL_COLUMN)) {
+                if (isStreamVirtualSlot(slot, 
Column.STREAM_SEQ_VIRTUAL_COLUMN)) {
                     project.add(new 
Alias(StatementScopeIdGenerator.newExprId(), tsoSlot, Column.STREAM_SEQ_COL));
                 }
+                if (isStreamVirtualSlot(slot, 
Column.STREAM_LSN_VIRTUAL_COLUMN)) {
+                    project.add(new 
Alias(StatementScopeIdGenerator.newExprId(), lsnSlot, Column.STREAM_LSN_COL));
+                }
             }
             historyPlan = new LogicalProject<>(project, plan);
         }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/ColumnDefinition.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/ColumnDefinition.java
index 77c12f8714b..c828b7e3176 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/ColumnDefinition.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/ColumnDefinition.java
@@ -724,6 +724,18 @@ public class ColumnDefinition {
         return columnDefinition;
     }
 
+    /**
+     * add hidden column __DORIS_ROW_LSN_COL__ for stable row identity on 
row-binlog tables.
+     */
+    public static ColumnDefinition newRowLsnColumnDefinition(AggregateType 
aggregateType) {
+        ColumnDefinition columnDefinition = new 
ColumnDefinition(Column.ROW_LSN_COL, BigIntType.INSTANCE, false,
+                    aggregateType, false, Optional.of(new 
DefaultValue(DefaultValue.ZERO_NUMBER)),
+                "doris row lsn hidden column", false);
+        columnDefinition.setEnableAddHiddenColumn(true);
+
+        return columnDefinition;
+    }
+
     /**
      * used in CreateTableInfo.validate(), specify the default value as 
DefaultValue.NULL_DEFAULT_VALUE
      * becasue ColumnDefinition.validate() will check that bitmap type column 
don't set default value
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
index 32b0d1c3869..993cf820c64 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
@@ -1628,16 +1628,17 @@ public class CreateTableInfo {
     }
 
     /**
-     * check if add Commit TSO Column
+     * Add hidden columns required by row binlog.
      */
-    public void createCommitTSOColumnIfNecessary(BinlogConfig binlogConfig) {
-        // __DORIS_COMMIT_TSO_COL__ injection for time-travel:
-        // only on dup / mow tables with row binlog enabled 
(binlog.enable=true && binlog.format=ROW).
-        if (keysType.equals(KeysType.DUP_KEYS)
-                || (keysType.equals(KeysType.UNIQUE_KEYS) && 
isEnableMergeOnWrite)) {
-            if (binlogConfig.isRowFormat()) {
-                
columns.add(ColumnDefinition.newCommitTsoColumnDefinition(AggregateType.NONE));
-            }
+    public void createRowBinlogHiddenColumnsIfNecessary(BinlogConfig 
binlogConfig) {
+        if (!binlogConfig.isRowFormat()) {
+            return;
+        }
+        if (keysType.equals(KeysType.DUP_KEYS)) {
+            
columns.add(ColumnDefinition.newCommitTsoColumnDefinition(AggregateType.NONE));
+            
columns.add(ColumnDefinition.newRowLsnColumnDefinition(AggregateType.NONE));
+        } else if (keysType.equals(KeysType.UNIQUE_KEYS) && 
isEnableMergeOnWrite) {
+            
columns.add(ColumnDefinition.newCommitTsoColumnDefinition(AggregateType.NONE));
         }
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapTableStreamScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapTableStreamScan.java
index b490684e858..48c40ce192a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapTableStreamScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapTableStreamScan.java
@@ -19,6 +19,7 @@ package org.apache.doris.nereids.trees.plans.logical;
 
 import org.apache.doris.analysis.TableScanParams;
 import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.KeysType;
 import org.apache.doris.catalog.OlapTable;
 import org.apache.doris.catalog.Table;
 import org.apache.doris.catalog.constraint.TableIdentifier;
@@ -129,9 +130,13 @@ public class LogicalOlapTableStreamScan extends 
LogicalOlapScan {
         if (cachedOutput.isPresent()) {
             return cachedOutput.get();
         }
-        // for reset, we could use get full schema of base table;
-        // otherwise, we only need to get the schema without hidden columns
-        List<Column> baseSchema = table.getBaseSchema(readMode == 
StreamReadMode.RESET);
+        // RESET and DUP_KEYS SNAPSHOT are rebuilt from the base table 
directly (no binlog union),
+        // so use the full schema to expose hidden columns like ROW_LSN_COL. 
Others only need visible.
+        boolean useFullSchemaScan = readMode == StreamReadMode.RESET
+                || (readMode == StreamReadMode.SNAPSHOT
+                        && table instanceof OlapTable
+                        && ((OlapTable) table).getKeysType() == 
KeysType.DUP_KEYS);
+        List<Column> baseSchema = table.getBaseSchema(useFullSchemaScan);
         List<SlotReference> slotFromColumn = createSlotsVectorized(baseSchema);
 
         ImmutableList.Builder<Slot> slots = ImmutableList.builder();
@@ -144,16 +149,16 @@ public class LogicalOlapTableStreamScan extends 
LogicalOlapScan {
                 continue;
             }
             Pair<Long, String> key = Pair.of(selectedIndexId, col.getName());
-            // For INCREMENTAL / SNAPSHOT reads, non-key value columns are 
materialized from the
-            // base table row-binlog whose after/before value columns are 
always nullable (see
+            // For INCREMENTAL / SNAPSHOT(MOW) reads, non-key value columns 
are materialized from
+            // the base table row-binlog whose after/before value columns are 
always nullable (see
             // Column.generateAfterValueColumn / generateBeforeValueColumn). 
Declare these value
             // columns as nullable here so the stream scan output stays 
consistent with the plan
             // expanded in NormalizeOlapTableStreamScan, otherwise 
AdjustNullable reports a
-            // not-nullable -> nullable conflict. RESET does a full base-table 
scan, so keep its
-            // original nullability.
+            // not-nullable -> nullable conflict. Full base scans (RESET / 
SNAPSHOT(DUP)) do a full
+            // base-table scan, so keep their original nullability.
             Slot slot = cacheSlotWithSlotName.computeIfAbsent(key, k -> {
                 SlotReference slotRef = slotFromColumn.get(index);
-                boolean forceNullable = readMode != StreamReadMode.RESET && 
!baseSchema.get(index).isKey();
+                boolean forceNullable = !useFullSchemaScan && 
!baseSchema.get(index).isKey();
                 return forceNullable ? slotRef.withNullable(true) : slotRef;
             });
             slots.add(slot);
@@ -174,6 +179,11 @@ public class LogicalOlapTableStreamScan extends 
LogicalOlapScan {
             // add stream exclusive virtual columns.
             slots.add(SlotReference.fromColumn(
                     exprIdGenerator.getNextId(), table, 
Column.STREAM_SEQ_VIRTUAL_COLUMN, qualified()));
+            // Only expose stream LSN when the base table stores row LSN, e.g. 
dup table with binlog.
+            if (table instanceof OlapTable && ((OlapTable) 
table).hasRowLsnColumn()) {
+                slots.add(SlotReference.fromColumn(
+                        exprIdGenerator.getNextId(), table, 
Column.STREAM_LSN_VIRTUAL_COLUMN, qualified()));
+            }
             slots.add(SlotReference.fromColumn(
                     exprIdGenerator.getNextId(), table, 
Column.STREAM_CHANGE_TYPE_VIRTUAL_COLUMN, qualified()));
         }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/task/CreateReplicaTask.java 
b/fe/fe-core/src/main/java/org/apache/doris/task/CreateReplicaTask.java
index 61c14853588..4a93c82eea6 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/task/CreateReplicaTask.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/task/CreateReplicaTask.java
@@ -316,6 +316,7 @@ public class CreateReplicaTask extends AgentTask {
         int sequenceCol = -1;
         int versionCol = -1;
         int commitTsoCol = -1;
+        int rowLsnCol = -1;
         List<TColumn> tColumns = null;
         Object tCols = objectPool.get(columns);
         if (tCols != null) {
@@ -357,12 +358,16 @@ public class CreateReplicaTask extends AgentTask {
             if (column.isCommitTsoColumn()) {
                 commitTsoCol = i;
             }
+            if (column.isRowLsnColumn()) {
+                rowLsnCol = i;
+            }
         }
         tSchema.setColumns(tColumns);
         tSchema.setDeleteSignIdx(deleteSign);
         tSchema.setSequenceColIdx(sequenceCol);
         tSchema.setVersionColIdx(versionCol);
         tSchema.setCommitTsoColIdx(commitTsoCol);
+        tSchema.setRowLsnColIdx(rowLsnCol);
         tSchema.setRowStoreColCids(rowStoreColumnUniqueIds);
         if (!CollectionUtils.isEmpty(clusterKeyUids)) {
             tSchema.setClusterKeyUids(clusterKeyUids);
diff --git a/gensrc/proto/internal_service.proto 
b/gensrc/proto/internal_service.proto
index b29608b15a4..00cd171dcc0 100644
--- a/gensrc/proto/internal_service.proto
+++ b/gensrc/proto/internal_service.proto
@@ -189,9 +189,8 @@ message PTabletWriterAddBlockRequest {
     // for auto-partition first stage close, we should hang.
     optional bool hang_wait = 15 [default = false];
     optional bool is_adaptive_random_bucket = 16 [default = false];
-    // Per-row row-binlog LSNs allocated by olap table sink. The order is the 
same
-    // as rows in block/tablet_ids.
-    repeated int64 row_binlog_lsns = 17;
+    // Per-row LSNs allocated by olap table sink. The order is the same as 
rows in block/tablet_ids.
+    repeated int64 allocated_lsns = 17;
 };
 
 message PSlaveTabletNodes {
diff --git a/gensrc/proto/olap_file.proto b/gensrc/proto/olap_file.proto
index 7ad1ff7135a..e2e503cac90 100644
--- a/gensrc/proto/olap_file.proto
+++ b/gensrc/proto/olap_file.proto
@@ -562,14 +562,17 @@ message TabletSchemaPB {
     // cid of the time-travel commit tso column (__DORIS_COMMIT_TSO_COL__).
     optional int32 commit_tso_col_idx = 39 [default = -1];
 
+    // cid of the base-table row lsn column (__DORIS_ROW_LSN_COL__).
+    optional int32 row_lsn_col_idx = 40 [default = -1];
+
     // cid of the binlog tso column (__DORIS_BINLOG_TSO__).
-    optional int32 binlog_tso_col_idx = 40 [default = -1];
+    optional int32 binlog_tso_col_idx = 41 [default = -1];
 
     // cid of the binlog lsn column (__DORIS_BINLOG_LSN__).
-    optional int32 binlog_lsn_col_idx = 41 [default = -1];
+    optional int32 binlog_lsn_col_idx = 42 [default = -1];
 
     // cid of the binlog op column (__DORIS_BINLOG_OP__).
-    optional int32 binlog_op_col_idx = 42 [default = -1];
+    optional int32 binlog_op_col_idx = 43 [default = -1];
 
     optional SplitSchemaPB __split_schema = 1000;  // A special field, DO NOT 
change it.
 }
@@ -621,14 +624,17 @@ message TabletSchemaCloudPB {
     // cid of the time-travel commit tso column (__DORIS_COMMIT_TSO_COL__).
     optional int32 commit_tso_col_idx = 40 [default = -1];
 
+    // cid of the base-table row lsn column (__DORIS_ROW_LSN_COL__).
+    optional int32 row_lsn_col_idx = 41 [default = -1];
+
     // cid of the binlog tso column (__DORIS_BINLOG_TSO__).
-    optional int32 binlog_tso_col_idx = 41 [default = -1];
+    optional int32 binlog_tso_col_idx = 42 [default = -1];
 
     // cid of the binlog lsn column (__DORIS_BINLOG_LSN__).
-    optional int32 binlog_lsn_col_idx = 42 [default = -1];
+    optional int32 binlog_lsn_col_idx = 43 [default = -1];
 
     // cid of the binlog op column (__DORIS_BINLOG_OP__).
-    optional int32 binlog_op_col_idx = 43 [default = -1];
+    optional int32 binlog_op_col_idx = 44 [default = -1];
 
     optional bool is_dynamic_schema = 100 [default=false];
 
diff --git a/gensrc/thrift/AgentService.thrift 
b/gensrc/thrift/AgentService.thrift
index fb689e12976..fb5c1a54931 100644
--- a/gensrc/thrift/AgentService.thrift
+++ b/gensrc/thrift/AgentService.thrift
@@ -53,9 +53,10 @@ struct TTabletSchema {
     24: optional i64 storage_dict_page_size = 262144
     25: optional list<Types.TColumnGroup> seq_map
     26: optional i32 commit_tso_col_idx = -1
-    27: optional i32 binlog_tso_idx = -1
-    28: optional i32 binlog_lsn_idx = -1
-    29: optional i32 binlog_op_idx = -1
+    27: optional i32 row_lsn_col_idx = -1
+    28: optional i32 binlog_tso_idx = -1
+    29: optional i32 binlog_lsn_idx = -1
+    30: optional i32 binlog_op_idx = -1
 }
 
 // this enum stands for different storage format in src_backends
diff --git a/regression-test/suites/row_binlog_p0/test_row_binlog_basic.groovy 
b/regression-test/suites/row_binlog_p0/test_row_binlog_basic.groovy
index 4bae6b52aef..1b7936bd592 100644
--- a/regression-test/suites/row_binlog_p0/test_row_binlog_basic.groovy
+++ b/regression-test/suites/row_binlog_p0/test_row_binlog_basic.groovy
@@ -200,6 +200,25 @@ suite("test_row_binlog_basic", "nonConcurrent") {
         ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__
     """
 
+    def dupRawLsnRows = sql """
+        SELECT k1, k2, k3, v1, v2, __DORIS_ROW_LSN_COL__
+        FROM test_dup_with_binlog
+        ORDER BY k1, k2, k3, v1, v2
+    """
+    def dupBinlogLsnRows = sql """
+        SELECT k1, k2, k3, v1, v2, __DORIS_BINLOG_LSN__
+        FROM binlog("table" = "test_dup_with_binlog")
+        ORDER BY k1, k2, k3, v1, v2
+    """
+    assertEquals(dupRawLsnRows.size(), dupBinlogLsnRows.size())
+    def dupRowLsns = dupRawLsnRows.collect { it[5] as long }
+    dupRowLsns.each { lsn -> assertTrue(lsn > 0, "row lsn should be positive 
but got ${lsn}") }
+    assertEquals(dupRowLsns.size(), dupRowLsns.toSet().size())
+    for (int i = 0; i < dupRawLsnRows.size(); i++) {
+        assertEquals(dupRawLsnRows[i][0..4], dupBinlogLsnRows[i][0..4])
+        assertEquals(dupRawLsnRows[i][5], dupBinlogLsnRows[i][5])
+    }
+
     sql """
         INSERT INTO test_mow_with_binlog VALUES
             (1, 1, 1, 10, '10'),
@@ -228,6 +247,14 @@ suite("test_row_binlog_basic", "nonConcurrent") {
         ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__
     """
 
+    test {
+        sql """
+            SELECT __DORIS_ROW_LSN_COL__
+            FROM test_mow_with_binlog
+        """
+        exception "Unknown column"
+    }
+
     sql """
         INSERT INTO test_mow_with_before_binlog VALUES
             (1, 1, 1, 10, '10'),
diff --git 
a/regression-test/suites/table_stream_p0/test_olap_table_stream_history_query.groovy
 
b/regression-test/suites/table_stream_p0/test_olap_table_stream_history_query.groovy
index e76bad400ed..6b88f29ec37 100644
--- 
a/regression-test/suites/table_stream_p0/test_olap_table_stream_history_query.groovy
+++ 
b/regression-test/suites/table_stream_p0/test_olap_table_stream_history_query.groovy
@@ -131,17 +131,32 @@ suite("test_olap_table_stream_history_query") {
 
     sql "SET show_hidden_columns=true;"
 
-    // verify select * exposes the hidden stream columns with the same real 
sequence value
-    def checkStreamHistoryWithHiddenColumns = { streamName ->
+    // verify hidden stream columns with the same real sequence value
+    def checkMowStreamHistoryWithHiddenColumns = { streamName ->
         long seq = fetchHistorySeq(streamName)
         assertEquals([[1, "s1", seq, "APPEND"], [2, "s2", seq, "APPEND"], [3, 
"s3", seq, "APPEND"]],
-                sql("select * from ${streamName} order by sid"))
+                sql("""select sid, sname, __DORIS_STREAM_SEQUENCE_COL__, 
__DORIS_STREAM_CHANGE_TYPE_COL__
+                        from ${streamName} order by sid"""))
+    }
+
+    def checkDupStreamHistoryWithHiddenColumns = { streamName ->
+        long seq = fetchHistorySeq(streamName)
+        def rows = sql("""select sid, sname, __DORIS_STREAM_SEQUENCE_COL__,
+                                  __DORIS_STREAM_LSN_COL__, 
__DORIS_STREAM_CHANGE_TYPE_COL__
+                           from ${streamName} order by sid""")
+        assertEquals([[1, "s1", seq, "APPEND"], [2, "s2", seq, "APPEND"], [3, 
"s3", seq, "APPEND"]],
+                rows.collect { [it[0], it[1], it[2], it[4]] })
+        def lsns = rows.collect { it[3] as long }
+        lsns.each { lsn -> assertTrue(lsn > 0, "stream lsn should be positive 
but got ${lsn}") }
+        for (int i = 1; i < lsns.size(); i++) {
+            assertTrue(lsns[i - 1] < lsns[i], "stream lsn should be increasing 
but got ${lsns}")
+        }
     }
 
     checkStreamHistory("s1")
     checkStreamHistory("s2")
-    checkStreamHistoryWithHiddenColumns("s1")
-    checkStreamHistoryWithHiddenColumns("s2")
+    checkMowStreamHistoryWithHiddenColumns("s1")
+    checkDupStreamHistoryWithHiddenColumns("s2")
 
     sql "DROP DATABASE IF EXISTS test_olap_table_stream_history_query_db"
 }
diff --git 
a/regression-test/suites/table_stream_p0/test_olap_table_stream_snapshot.groovy 
b/regression-test/suites/table_stream_p0/test_olap_table_stream_snapshot.groovy
index 511f338438e..6d13d7904c4 100644
--- 
a/regression-test/suites/table_stream_p0/test_olap_table_stream_snapshot.groovy
+++ 
b/regression-test/suites/table_stream_p0/test_olap_table_stream_snapshot.groovy
@@ -62,6 +62,13 @@ suite("test_olap_table_stream_snapshot", "nonConcurrent") {
             exception "__DORIS_STREAM_SEQUENCE_COL__"
         }
     }
+    // DUP snapshot exposes base row LSN.
+    def checkDupSnapshotLsn = { streamName ->
+        def lsns = sql("SELECT __DORIS_ROW_LSN_COL__ FROM 
${streamName}@snapshot() ORDER BY id")
+                .collect { it[0] as long }
+        assertTrue(lsns.size() > 0)
+        lsns.each { lsn -> assertTrue(lsn > 0, "snapshot row lsn should be 
positive but got ${lsn}") }
+    }
 
     // 1) DUP + append_only + show_initial_rows=true + non-partitioned table.
     // snapshot reads the stream creation snapshot and does not advance the 
stream offset.
@@ -136,6 +143,7 @@ suite("test_olap_table_stream_snapshot", "nonConcurrent") {
     waitVisible()
     checkRows([["1", "10"], ["2", "20"]],
             "SELECT id, v FROM s_dup_np_false@snapshot() ORDER BY id")
+    checkDupSnapshotLsn("s_dup_np_false")
     checkRows([["3", "30"]],
             "SELECT id, v FROM s_dup_np_false ORDER BY id")
     sql """
diff --git 
a/regression-test/suites/table_stream_p0/test_table_stream_query_comprehensive.groovy
 
b/regression-test/suites/table_stream_p0/test_table_stream_query_comprehensive.groovy
index 9273e709ffc..32277b32af3 100644
--- 
a/regression-test/suites/table_stream_p0/test_table_stream_query_comprehensive.groovy
+++ 
b/regression-test/suites/table_stream_p0/test_table_stream_query_comprehensive.groovy
@@ -172,6 +172,23 @@ suite("test_table_stream_query_comprehensive", 
"nonConcurrent") {
         assertEquals(0, sql("SELECT id, v FROM dup_ao_stream").size())
         order_qt_dup_ao_target "SELECT id, v FROM dup_ao_target ORDER BY id"
 
+        // A.5 incremental DUP stream exposes positive and unique per-row LSNs.
+        sql "INSERT INTO dup_ao_base VALUES (6, 'f'), (7, 'g')"
+        sql "INSERT INTO dup_ao_base VALUES (8, 'h'), (9, 'i')"
+        sql "INSERT INTO dup_ao_base VALUES (10, 'j'), (11, 'k')"
+        sql "sync"
+        sleep(1200)
+        def streamLsnRows = sql """
+            SELECT __DORIS_STREAM_SEQUENCE_COL__, __DORIS_STREAM_LSN_COL__
+            FROM dup_ao_stream
+            ORDER BY __DORIS_STREAM_SEQUENCE_COL__, __DORIS_STREAM_LSN_COL__
+        """
+        assertEquals(6, streamLsnRows.size())
+        def streamLsns = streamLsnRows.collect { it[1] as long }
+        streamLsns.each { lsn -> assertTrue(lsn > 0, "stream lsn should be 
positive but got ${lsn}") }
+        assertEquals(streamLsns.size(), streamLsns.toSet().size())
+        assertEquals([2, 2, 2], streamLsnRows.groupBy { it[0] 
}.values().collect { it.size() }.sort())
+
         // ============================================================
         // Section B. DUP, non-partitioned, append_only, initial=true.
         //   Focus: historical seed rows are returned first (APPEND), then


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

Reply via email to