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 7b7cea2e98d [opt](partial update) Select publish-conflict read
strategy by update mode (#67295)
7b7cea2e98d is described below
commit 7b7cea2e98d68f1019490c199645c16b120b4436
Author: foxtail463 <[email protected]>
AuthorDate: Wed Sep 16 20:18:14 2026 +0800
[opt](partial update) Select publish-conflict read strategy by update mode
(#67295)
# Problem Summary:
FixedReadPlan previously preferred the row store whenever a complete
row-store column was available, regardless of the caller’s update mode.
This caused fixed partial-update conflict reconstruction to materialize
and decode the complete JSONB row when only the updated columns were
required. However, globally switching to physical-column reads would
break UPSERT semantics for values such as raw Variant data.
Physical-column reads also repeated segment setup for each requested
column.
# Solution:
Introduce an explicit read strategy selected according to the update
mode. Fixed partial updates read their current projection from physical
columns, while UPSERT and historical, flexible, sequence, and row-binlog
paths retain row-store-first behavior. Batch physical-column reads so
each planned segment is loaded once and column iterators are created and
released sequentially.
# benchmark:
A RELEASE warm-cache microbenchmark compared row-store and
physical-column reads on an 8,192-row segment with 100 mixed STRING/INT
value columns.
For 8,192 conflict rows, physical-column reads improved latency by
30.21x, 7.59x, and 5.59x for 2-, 50-, and 100-column projections, while
reducing tracked peak memory by 96.7%, 52.9%, and 35.8%, respectively.
For sparse 64-row conflicts, physical columns were 5.10x faster for 2
columns, but slower for wide 50- and 100-column projections because
per-column setup dominated. This is a focused read-path microbenchmark,
not an end-to-end publish throughput test.
Co-authored-by: yangtao555 <[email protected]>
---
be/src/storage/mow/historical_row_fetcher.cpp | 5 +-
be/src/storage/partial_update_info.cpp | 35 ++--
be/src/storage/partial_update_info.h | 9 +-
be/src/storage/tablet/base_tablet.cpp | 116 ++++++++---
be/src/storage/tablet/base_tablet.h | 6 +
.../storage/mow/historical_row_fetcher_test.cpp | 138 +++++++++++++
.../storage/transform/variant_rowstore_test.cpp | 227 +++++++++++++++++++++
7 files changed, 490 insertions(+), 46 deletions(-)
diff --git a/be/src/storage/mow/historical_row_fetcher.cpp
b/be/src/storage/mow/historical_row_fetcher.cpp
index 974099b8c83..4b66468a0fa 100644
--- a/be/src/storage/mow/historical_row_fetcher.cpp
+++ b/be/src/storage/mow/historical_row_fetcher.cpp
@@ -72,8 +72,9 @@ Status HistoricalRowFetcher::read_columns(const TabletSchema&
tablet_schema,
bool force_read_old_delete_signs,
const signed char* __restrict
cur_delete_signs) const {
return _fixed_plan.read_columns_by_plan(tablet_schema,
std::move(cids_to_read), _rsid_to_rowset,
- dst_block, read_index,
force_read_old_delete_signs,
- cur_delete_signs);
+ dst_block, read_index,
+
FixedReadPlan::ReadStrategy::PREFER_ROW_STORE,
+ force_read_old_delete_signs,
cur_delete_signs);
}
} // namespace doris
diff --git a/be/src/storage/partial_update_info.cpp
b/be/src/storage/partial_update_info.cpp
index 535021aa93b..c8fd16811b0 100644
--- a/be/src/storage/partial_update_info.cpp
+++ b/be/src/storage/partial_update_info.cpp
@@ -356,8 +356,8 @@ void FixedReadPlan::prepare_to_read(const RowLocation&
row_location, size_t pos)
Status FixedReadPlan::read_columns_by_plan(
const TabletSchema& tablet_schema, std::vector<uint32_t> cids_to_read,
const std::map<RowsetId, RowsetSharedPtr>& rsid_to_rowset, Block&
block,
- std::map<uint32_t, uint32_t>* read_index, bool
force_read_old_delete_signs,
- const signed char* __restrict cur_delete_signs) const {
+ std::map<uint32_t, uint32_t>* read_index, ReadStrategy read_strategy,
+ bool force_read_old_delete_signs, const signed char* __restrict
cur_delete_signs) const {
if (force_read_old_delete_signs) {
// always read delete sign column from historical data
if (block.get_position_by_name(DELETE_SIGN) == -1) {
@@ -366,10 +366,11 @@ Status FixedReadPlan::read_columns_by_plan(
block.swap(tablet_schema.create_storage_block(cids_to_read));
}
}
- bool has_row_column = tablet_schema.has_row_store_for_all_columns();
+ const bool use_row_store = read_strategy == ReadStrategy::PREFER_ROW_STORE
&&
+ tablet_schema.has_row_store_for_all_columns();
std::optional<Block::ScopedMutableColumns> mutable_columns_guard;
MutableColumns* mutable_columns = nullptr;
- if (!has_row_column) {
+ if (!use_row_store) {
mutable_columns_guard.emplace(block);
mutable_columns = &mutable_columns_guard->mutable_columns();
}
@@ -386,7 +387,7 @@ Status FixedReadPlan::read_columns_by_plan(
rids.emplace_back(rid);
(*read_index)[static_cast<uint32_t>(pos)] = read_idx++;
}
- if (has_row_column) {
+ if (use_row_store) {
auto st = BaseTablet::fetch_value_through_row_column(
rowset_iter->second, tablet_schema, segment_id, rids,
cids_to_read, block);
if (!st.ok()) {
@@ -395,16 +396,12 @@ Status FixedReadPlan::read_columns_by_plan(
}
continue;
}
- for (size_t cid = 0; cid < mutable_columns->size(); ++cid) {
- TabletColumn tablet_column =
tablet_schema.column(cids_to_read[cid]);
- auto st =
doris::BaseTablet::fetch_value_by_rowids(rowset_iter->second, segment_id,
- rids,
tablet_column,
-
(*mutable_columns)[cid]);
- // set read value to output block
- if (!st.ok()) {
- LOG(WARNING) << "failed to fetch value";
- return st;
- }
+ auto st = BaseTablet::fetch_values_by_rowids(rowset_iter->second,
tablet_schema,
+ segment_id, rids,
cids_to_read,
+ *mutable_columns);
+ if (!st.ok()) {
+ LOG(WARNING) << "failed to fetch values by rowids";
+ return st;
}
}
}
@@ -462,7 +459,8 @@ Status FixedReadPlan::fill_missing_columns(
// segment pos to write -> rowid to read in old_value_block
std::map<uint32_t, uint32_t> read_index;
RETURN_IF_ERROR(read_columns_by_plan(tablet_schema, missing_cids,
rsid_to_rowset,
- old_value_block, &read_index, true,
nullptr));
+ old_value_block, &read_index,
+ ReadStrategy::PREFER_ROW_STORE, true,
nullptr));
const auto* old_delete_sign_column_data =
BaseTablet::get_delete_sign_column_data(old_value_block);
@@ -1158,8 +1156,9 @@ Status BlockAggregator::fill_sequence_column(Block*
block, size_t num_rows,
auto seq_col_block = _tablet_schema.create_storage_block(cids);
auto tmp_block = _tablet_schema.create_storage_block(cids);
std::map<uint32_t, uint32_t> read_index;
- RETURN_IF_ERROR(read_plan.read_columns_by_plan(_tablet_schema, cids,
_fetcher.pinned_rowsets(),
- seq_col_block, &read_index,
false));
+ RETURN_IF_ERROR(read_plan.read_columns_by_plan(
+ _tablet_schema, cids, _fetcher.pinned_rowsets(), seq_col_block,
&read_index,
+ FixedReadPlan::ReadStrategy::PREFER_ROW_STORE, false));
auto new_seq_col_ptr =
tmp_block.get_by_position(0).column->assert_mutable();
const auto& old_seq_col_ptr = *seq_col_block.get_by_position(0).column;
diff --git a/be/src/storage/partial_update_info.h
b/be/src/storage/partial_update_info.h
index 4b8ab99840f..f5341c4f559 100644
--- a/be/src/storage/partial_update_info.h
+++ b/be/src/storage/partial_update_info.h
@@ -127,6 +127,13 @@ struct RidAndPos {
class FixedReadPlan {
public:
+ enum class ReadStrategy {
+ // Use the full row-store column when available; otherwise read
physical columns.
+ PREFER_ROW_STORE,
+ // Read only the requested physical columns, even when a full
row-store column exists.
+ COLUMN_STORE,
+ };
+
bool empty() const;
void clear() { plan.clear(); }
void prepare_to_read(const RowLocation& row_location, size_t pos);
@@ -134,7 +141,7 @@ public:
std::vector<uint32_t> cids_to_read,
const std::map<RowsetId, RowsetSharedPtr>&
rsid_to_rowset,
Block& block, std::map<uint32_t, uint32_t>*
read_index,
- bool force_read_old_delete_signs,
+ ReadStrategy read_strategy, bool
force_read_old_delete_signs,
const signed char* __restrict cur_delete_signs
= nullptr) const;
Status fill_missing_columns(const
segment_v2::HistoricalRowRetrieverContext& historical_context,
const std::map<RowsetId, RowsetSharedPtr>&
rsid_to_rowset,
diff --git a/be/src/storage/tablet/base_tablet.cpp
b/be/src/storage/tablet/base_tablet.cpp
index 2084b3abbb3..fcdd19416c1 100644
--- a/be/src/storage/tablet/base_tablet.cpp
+++ b/be/src/storage/tablet/base_tablet.cpp
@@ -79,24 +79,31 @@ bvar::LatencyRecorder
g_tablet_update_delete_bitmap_latency("doris_pk", "update_
static bvar::Adder<size_t> g_total_tablet_num("doris_total_tablet_num");
-Status _get_segment_column_iterator(const BetaRowsetSharedPtr& rowset,
uint32_t segid,
- const TabletColumn& target_column,
- SegmentCacheHandle* segment_cache_handle,
-
std::unique_ptr<segment_v2::ColumnIterator>* column_iterator,
- OlapReaderStatistics* stats,
- const io::IOContext* input_io_ctx =
nullptr) {
+Status _load_segment(const BetaRowsetSharedPtr& rowset, uint32_t segid,
+ SegmentCacheHandle* segment_cache_handle,
+ segment_v2::SegmentSharedPtr* segment,
OlapReaderStatistics* stats,
+ const io::IOContext* input_io_ctx = nullptr) {
RETURN_IF_ERROR(SegmentLoader::instance()->load_segments(rowset,
segment_cache_handle, true,
false, stats,
input_io_ctx));
- // find segment
- auto it = std::find_if(
- segment_cache_handle->get_segments().begin(),
- segment_cache_handle->get_segments().end(),
- [&segid](const segment_v2::SegmentSharedPtr& seg) { return
seg->id() == segid; });
+ auto it = std::find_if(segment_cache_handle->get_segments().begin(),
+ segment_cache_handle->get_segments().end(),
+ [segid](const segment_v2::SegmentSharedPtr&
candidate) {
+ return candidate->id() == segid;
+ });
if (it == segment_cache_handle->get_segments().end()) {
- return Status::NotFound(fmt::format("rowset {} 's segemnt not found,
seg_id {}",
+ return Status::NotFound(fmt::format("rowset {}'s segment not found,
seg_id {}",
rowset->rowset_id().to_string(),
segid));
}
- segment_v2::SegmentSharedPtr segment = *it;
+ *segment = *it;
+ TEST_SYNC_POINT_CALLBACK("BaseTablet::_load_segment", rowset.get(),
&segid);
+ return Status::OK();
+}
+
+Status _init_segment_column_iterator(const segment_v2::SegmentSharedPtr&
segment,
+ const TabletColumn& target_column,
+
std::unique_ptr<segment_v2::ColumnIterator>* column_iterator,
+ OlapReaderStatistics* stats,
+ const io::IOContext* input_io_ctx =
nullptr) {
StorageReadOptions opts;
opts.stats = stats;
if (input_io_ctx != nullptr) {
@@ -116,6 +123,19 @@ Status _get_segment_column_iterator(const
BetaRowsetSharedPtr& rowset, uint32_t
return Status::OK();
}
+Status _get_segment_column_iterator(const BetaRowsetSharedPtr& rowset,
uint32_t segid,
+ const TabletColumn& target_column,
+ SegmentCacheHandle* segment_cache_handle,
+
std::unique_ptr<segment_v2::ColumnIterator>* column_iterator,
+ OlapReaderStatistics* stats,
+ const io::IOContext* input_io_ctx =
nullptr) {
+ segment_v2::SegmentSharedPtr segment;
+ RETURN_IF_ERROR(
+ _load_segment(rowset, segid, segment_cache_handle, &segment,
stats, input_io_ctx));
+ return _init_segment_column_iterator(segment, target_column,
column_iterator, stats,
+ input_io_ctx);
+}
+
} // namespace
extern MetricPrototype METRIC_query_scan_bytes;
@@ -865,9 +885,9 @@ Status
BaseTablet::calc_segment_delete_bitmap(RowsetSharedPtr rowset,
std::map<RowsetId, RowsetSharedPtr> rsid_to_row_binlog {
{row_binlog_rowset->rowset_id(), row_binlog_rowset}};
std::map<uint32_t, uint32_t> read_index;
-
RETURN_IF_ERROR(read_plan_lsn.read_columns_by_plan(*row_binlog_schema, lsn_cids,
-
rsid_to_row_binlog, lsn_block,
- &read_index,
false));
+ RETURN_IF_ERROR(read_plan_lsn.read_columns_by_plan(
+ *row_binlog_schema, lsn_cids, rsid_to_row_binlog,
lsn_block, &read_index,
+ FixedReadPlan::ReadStrategy::PREFER_ROW_STORE, false));
}
std::vector<uint32_t> sort_perm;
@@ -974,6 +994,7 @@ Status
BaseTablet::fetch_value_through_row_column(RowsetSharedPtr input_rowset,
BetaRowsetSharedPtr rowset =
std::static_pointer_cast<BetaRowset>(input_rowset);
CHECK(rowset);
+ TEST_SYNC_POINT_CALLBACK("BaseTablet::fetch_value_through_row_column",
rowset.get());
CHECK(tablet_schema.has_row_store_for_all_columns());
SegmentCacheHandle segment_cache_handle;
std::unique_ptr<segment_v2::ColumnIterator> column_iterator;
@@ -1003,6 +1024,37 @@ Status
BaseTablet::fetch_value_through_row_column(RowsetSharedPtr input_rowset,
return Status::OK();
}
+Status BaseTablet::fetch_values_by_rowids(RowsetSharedPtr input_rowset,
+ const TabletSchema& tablet_schema,
uint32_t segid,
+ const std::vector<uint32_t>& rowids,
+ const std::vector<uint32_t>& cids,
+ MutableColumns& dst_columns) {
+ MonotonicStopWatch watch;
+ watch.start();
+ Defer _defer([&]() {
+ LOG_EVERY_N(INFO, 500) << "fetch_values_by_rowids, cost(us):" <<
watch.elapsed_time() / 1000
+ << ", row_batch_size:" << rowids.size()
+ << ", column_count:" << cids.size();
+ });
+
+ BetaRowsetSharedPtr rowset =
std::static_pointer_cast<BetaRowset>(input_rowset);
+ CHECK(rowset);
+ TEST_SYNC_POINT_CALLBACK("BaseTablet::fetch_values_by_rowids",
rowset.get(), &cids);
+ CHECK_EQ(cids.size(), dst_columns.size());
+ SegmentCacheHandle segment_cache_handle;
+ OlapReaderStatistics stats;
+ segment_v2::SegmentSharedPtr segment;
+ RETURN_IF_ERROR(_load_segment(rowset, segid, &segment_cache_handle,
&segment, &stats));
+ for (size_t i = 0; i < cids.size(); ++i) {
+ std::unique_ptr<segment_v2::ColumnIterator> column_iterator;
+ RETURN_IF_ERROR(_init_segment_column_iterator(segment,
tablet_schema.column(cids[i]),
+ &column_iterator,
&stats));
+ RETURN_IF_ERROR(
+ column_iterator->read_by_rowids(rowids.data(), rowids.size(),
dst_columns[i]));
+ }
+ return Status::OK();
+}
+
Status BaseTablet::fetch_value_by_rowids(RowsetSharedPtr input_rowset,
uint32_t segid,
const std::vector<uint32_t>& rowids,
const TabletColumn& tablet_column,
MutableColumnPtr& dst) {
@@ -1083,10 +1135,22 @@ Status
BaseTablet::generate_new_block_for_partial_update(
// rowid in the final block(start from 0, increase continuously) -> rowid
to read in update_block
std::map<uint32_t, uint32_t> read_index_update;
- // read current rowset first, if a row in the current rowset has delete
sign mark
- // we don't need to read values from old block
+ // Row-store and physical Variant columns are not always
representation-equivalent. A typed
+ // Variant path can be coerced by the column writer after RowStoreFill
(for example, string
+ // "001" becomes integer 1), so rebuilding a conflicting row from physical
Variant columns
+ // would make its row-store value depend on whether a publish conflict
occurred. Keep the
+ // row-store path for projections containing Variant; fixed updates of
ordinary columns can use
+ // the narrower physical-column read.
+ const bool update_contains_variant = std::ranges::any_of(update_cids,
[&](uint32_t cid) {
+ return rowset_schema->column(cid).is_variant_type();
+ });
+ const auto update_read_strategy =
+ partial_update_info->is_fixed_partial_update() &&
!update_contains_variant
+ ? FixedReadPlan::ReadStrategy::COLUMN_STORE
+ : FixedReadPlan::ReadStrategy::PREFER_ROW_STORE;
RETURN_IF_ERROR(read_plan_update.read_columns_by_plan(
- *rowset_schema, update_cids, rsid_to_rowset, update_block,
&read_index_update, false));
+ *rowset_schema, update_cids, rsid_to_rowset, update_block,
&read_index_update,
+ update_read_strategy, false));
size_t update_rows = read_index_update.size();
for (auto i = 0; i < update_cids.size(); ++i) {
for (auto idx = 0; idx < update_rows; ++idx) {
@@ -1104,9 +1168,9 @@ Status BaseTablet::generate_new_block_for_partial_update(
// rowid in the final block(start from 0, increase, may not continuous
becasue we skip to read some rows) -> rowid to read in old_block
std::map<uint32_t, uint32_t> read_index_old;
- RETURN_IF_ERROR(read_plan_ori.read_columns_by_plan(*rowset_schema,
missing_cids, rsid_to_rowset,
- old_block,
&read_index_old, true,
-
new_block_delete_signs));
+ RETURN_IF_ERROR(read_plan_ori.read_columns_by_plan(
+ *rowset_schema, missing_cids, rsid_to_rowset, old_block,
&read_index_old,
+ FixedReadPlan::ReadStrategy::PREFER_ROW_STORE, true,
new_block_delete_signs));
size_t old_rows = read_index_old.size();
const auto* __restrict old_block_delete_signs =
get_delete_sign_column_data(old_block, old_rows);
@@ -1245,8 +1309,9 @@ Status
BaseTablet::generate_new_block_for_flexible_partial_update(
// 1. read the current rowset first, if a row in the current rowset has
delete sign mark
// we don't need to read values from old block for that row
- RETURN_IF_ERROR(read_plan_update.read_columns_by_plan(*rowset_schema,
all_cids, rsid_to_rowset,
- update_block,
&read_index_update, true));
+ RETURN_IF_ERROR(read_plan_update.read_columns_by_plan(
+ *rowset_schema, all_cids, rsid_to_rowset, update_block,
&read_index_update,
+ FixedReadPlan::ReadStrategy::PREFER_ROW_STORE, true));
size_t update_rows = read_index_update.size();
// TODO(bobhan1): add the delete sign optimazation here
@@ -1261,7 +1326,8 @@ Status
BaseTablet::generate_new_block_for_flexible_partial_update(
// rowid in the final block(start from 0, increase, may not continuous
becasue we skip to read some rows) -> rowid to read in old_block
std::map<uint32_t, uint32_t> read_index_old;
RETURN_IF_ERROR(read_plan_ori.read_columns_by_plan(
- *rowset_schema, non_sort_key_cids, rsid_to_rowset, old_block,
&read_index_old, true));
+ *rowset_schema, non_sort_key_cids, rsid_to_rowset, old_block,
&read_index_old,
+ FixedReadPlan::ReadStrategy::PREFER_ROW_STORE, true));
size_t old_rows = read_index_old.size();
DCHECK(update_rows == old_rows);
const auto* __restrict old_block_delete_signs =
diff --git a/be/src/storage/tablet/base_tablet.h
b/be/src/storage/tablet/base_tablet.h
index 0bf620b7e15..769f9bb28e2 100644
--- a/be/src/storage/tablet/base_tablet.h
+++ b/be/src/storage/tablet/base_tablet.h
@@ -251,6 +251,12 @@ public:
const std::vector<uint32_t>&
rowids,
const std::vector<uint32_t>&
cids, Block& block);
+ static Status fetch_values_by_rowids(RowsetSharedPtr input_rowset,
+ const TabletSchema& tablet_schema,
uint32_t segid,
+ const std::vector<uint32_t>& rowids,
+ const std::vector<uint32_t>& cids,
+ MutableColumns& dst_columns);
+
static Status fetch_value_by_rowids(RowsetSharedPtr input_rowset, uint32_t
segid,
const std::vector<uint32_t>& rowids,
const TabletColumn& tablet_column,
MutableColumnPtr& dst);
diff --git a/be/test/storage/mow/historical_row_fetcher_test.cpp
b/be/test/storage/mow/historical_row_fetcher_test.cpp
index 3a931cbe333..3e81bced3ea 100644
--- a/be/test/storage/mow/historical_row_fetcher_test.cpp
+++ b/be/test/storage/mow/historical_row_fetcher_test.cpp
@@ -23,6 +23,7 @@
#include <memory>
#include <vector>
+#include "cpp/sync_point.h"
#include "storage/mow/mow_transform_test_base.h"
#include "storage/partial_update_info.h"
@@ -80,6 +81,143 @@ TEST_F(HistoricalRowFetcherTest,
ReadColumnsReturnsThePlannedRows) {
EXPECT_EQ(read_int(old_values, 0, read_index[1]), 11); // dst 1 <- row 0
}
+// A full row-store schema can still read a narrow projection directly from
physical columns.
+// Both sources must preserve planned row order and delete-sign semantics.
+TEST_F(HistoricalRowFetcherTest, FixedPlanColumnStoreReadMatchesRowStore) {
+ auto schema = create_row_store_schema();
+ TabletSharedPtr tablet;
+ auto rowset = write_rowset(schema, 5002, 2, {{1, 11, 0, 0}, {2, 22, 0,
1}}, &tablet);
+ std::map<RowsetId, RowsetSharedPtr> rowsets {{rowset->rowset_id(),
rowset}};
+
+ FixedReadPlan read_plan;
+ read_plan.prepare_to_read(RowLocation {rowset->rowset_id(), 0, 1},
/*dst_pos=*/0);
+ read_plan.prepare_to_read(RowLocation {rowset->rowset_id(), 0, 0},
/*dst_pos=*/1);
+
+ const std::vector<uint32_t> cids {0, 1};
+ auto column_store_block = schema->create_storage_block(cids);
+ auto row_store_block = schema->create_storage_block(cids);
+ std::map<uint32_t, uint32_t> column_store_read_index;
+ std::map<uint32_t, uint32_t> row_store_read_index;
+
+ ASSERT_TRUE(read_plan
+ .read_columns_by_plan(*schema, cids, rowsets,
column_store_block,
+ &column_store_read_index,
+
FixedReadPlan::ReadStrategy::COLUMN_STORE,
+
/*force_read_old_delete_signs=*/true)
+ .ok());
+ ASSERT_TRUE(read_plan
+ .read_columns_by_plan(*schema, cids, rowsets,
row_store_block,
+ &row_store_read_index,
+
FixedReadPlan::ReadStrategy::PREFER_ROW_STORE,
+
/*force_read_old_delete_signs=*/true)
+ .ok());
+
+ EXPECT_EQ(column_store_read_index, row_store_read_index);
+ EXPECT_EQ(column_store_block.dump_data(), row_store_block.dump_data());
+ ASSERT_EQ(column_store_block.rows(), 2);
+ EXPECT_EQ(read_int(column_store_block, 0, 0), 2);
+ EXPECT_EQ(read_int(column_store_block, 0, 1), 1);
+ EXPECT_EQ(read_int(column_store_block, 1, 0), 22);
+ EXPECT_EQ(read_int(column_store_block, 1, 1), 11);
+ EXPECT_EQ(read_tinyint(column_store_block, 2, 0), 1);
+ EXPECT_EQ(read_tinyint(column_store_block, 2, 1), 0);
+}
+
+// The production publish-conflict rebuild must read the current rowset's
update projection from
+// physical columns, while the historical missing projection still uses the
row store. The batch
+// physical read must load/locate each planned segment once, not once per
update column.
+TEST_F(HistoricalRowFetcherTest,
PublishConflictUsesBatchedColumnStoreForCurrentProjection) {
+ auto schema = create_row_store_schema(/*has_seq=*/true);
+ TabletSharedPtr tablet;
+ auto current_rowset = write_rowset(schema, 5003, 3, {{1, 101, 5, 0}},
&tablet);
+ auto historical_rowset = write_rowset(schema, 5004, 2, {{1, 11, 3, 0}},
&tablet);
+
+ auto partial_update_info = std::make_shared<PartialUpdateInfo>();
+ ASSERT_TRUE(partial_update_info
+ ->init(kTabletId, /*txn_id=*/1, *schema,
+ UniqueKeyUpdateModePB::UPDATE_FIXED_COLUMNS,
+ PartialUpdateNewRowPolicyPB::APPEND, {"k", "v"},
+ /*is_strict_mode=*/false, /*timestamp_ms=*/0,
+ /*nano_seconds=*/0, "UTC", "")
+ .ok());
+
+ FixedReadPlan read_plan_update;
+ read_plan_update.prepare_to_read(
+ RowLocation {current_rowset->rowset_id(), /*segment_id=*/0,
/*row_id=*/0},
+ /*dst_pos=*/0);
+ FixedReadPlan read_plan_historical;
+ read_plan_historical.prepare_to_read(
+ RowLocation {historical_rowset->rowset_id(), /*segment_id=*/0,
/*row_id=*/0},
+ /*dst_pos=*/0);
+ std::map<RowsetId, RowsetSharedPtr> rowsets {
+ {current_rowset->rowset_id(), current_rowset},
+ {historical_rowset->rowset_id(), historical_rowset}};
+
+ int current_segment_loads = 0;
+ int historical_segment_loads = 0;
+ int current_batch_reads = 0;
+ int current_row_store_reads = 0;
+ int historical_row_store_reads = 0;
+ size_t current_batch_column_count = 0;
+ auto* sync_point = SyncPoint::get_instance();
+ SyncPoint::CallbackGuard load_segment_guard;
+ SyncPoint::CallbackGuard batch_read_guard;
+ SyncPoint::CallbackGuard row_store_read_guard;
+ sync_point->set_call_back(
+ "BaseTablet::_load_segment",
+ [&](auto&& args) {
+ auto* rowset = try_any_cast<BetaRowset*>(args[0]);
+ if (rowset->rowset_id() == current_rowset->rowset_id()) {
+ ++current_segment_loads;
+ } else if (rowset->rowset_id() ==
historical_rowset->rowset_id()) {
+ ++historical_segment_loads;
+ }
+ },
+ &load_segment_guard);
+ sync_point->set_call_back(
+ "BaseTablet::fetch_values_by_rowids",
+ [&](auto&& args) {
+ auto* rowset = try_any_cast<BetaRowset*>(args[0]);
+ if (rowset->rowset_id() == current_rowset->rowset_id()) {
+ ++current_batch_reads;
+ current_batch_column_count =
+ try_any_cast<const
std::vector<uint32_t>*>(args[1])->size();
+ }
+ },
+ &batch_read_guard);
+ sync_point->set_call_back(
+ "BaseTablet::fetch_value_through_row_column",
+ [&](auto&& args) {
+ auto* rowset = try_any_cast<BetaRowset*>(args[0]);
+ if (rowset->rowset_id() == current_rowset->rowset_id()) {
+ ++current_row_store_reads;
+ } else if (rowset->rowset_id() ==
historical_rowset->rowset_id()) {
+ ++historical_row_store_reads;
+ }
+ },
+ &row_store_read_guard);
+ sync_point->enable_processing();
+
+ auto output_block = schema->create_storage_block();
+ auto st = BaseTablet::generate_new_block_for_partial_update(
+ schema, partial_update_info.get(), read_plan_historical,
read_plan_update, rowsets,
+ &output_block);
+ sync_point->disable_processing();
+
+ ASSERT_TRUE(st.ok()) << st;
+ ASSERT_EQ(output_block.rows(), 1);
+ EXPECT_EQ(read_int(output_block, 0, 0), 1);
+ EXPECT_EQ(read_int(output_block, 1, 0), 101);
+ EXPECT_EQ(read_int(output_block, 2, 0), 3);
+ EXPECT_EQ(read_tinyint(output_block, 3, 0), 0);
+ EXPECT_EQ(current_batch_reads, 1);
+ EXPECT_EQ(current_batch_column_count, 2);
+ EXPECT_EQ(current_row_store_reads, 0);
+ EXPECT_EQ(historical_row_store_reads, 1);
+ EXPECT_EQ(current_segment_loads, 1);
+ EXPECT_EQ(historical_segment_loads, 1);
+}
+
// The fixed partial update fill: rows flagged for a historical read take the
old value, rows
// flagged use-default take the column default.
TEST_F(HistoricalRowFetcherTest, FillMissingColumnsMixesHistoryAndDefaults) {
diff --git a/be/test/storage/transform/variant_rowstore_test.cpp
b/be/test/storage/transform/variant_rowstore_test.cpp
index 48f95dc8c1b..2750ac5af0e 100644
--- a/be/test/storage/transform/variant_rowstore_test.cpp
+++ b/be/test/storage/transform/variant_rowstore_test.cpp
@@ -21,6 +21,7 @@
#include <gtest/gtest.h>
#include <limits>
+#include <numeric>
#include <string>
#include <string_view>
#include <unordered_map>
@@ -35,8 +36,10 @@
#include "core/data_type_serde/data_type_variant_v2_serde.h"
#include "core/field.h"
#include "core/string_buffer.hpp"
+#include "cpp/sync_point.h"
#include "storage/mow/mow_transform_test_base.h"
#include "storage/rowset/rowset_writer_context.h"
+#include "storage/tablet/base_tablet.h"
#include "storage/transform/block_transform.h"
#include "testutil/variant_util.h"
#include "util/jsonb/serialize.h"
@@ -113,6 +116,35 @@ protected:
return schema;
}
+ TabletSchemaSPtr create_typed_variant_pu_row_store_schema() {
+ TabletSchemaPB pb;
+ create_variant_pu_schema()->to_schema_pb(&pb);
+ pb.set_store_row_column(true);
+ ColumnPB* row_store = pb.add_column();
+ row_store->set_unique_id(10);
+ row_store->set_name(BeConsts::ROW_STORE_COL);
+ row_store->set_type("STRING");
+ row_store->set_is_key(false);
+ row_store->set_length(2147483643);
+ row_store->set_index_length(4);
+ row_store->set_is_nullable(false);
+ row_store->set_aggregation("NONE");
+
+ auto schema = std::make_shared<TabletSchema>();
+ schema->init_from_pb(pb);
+
+ ColumnPB typed_path_pb;
+ typed_path_pb.set_unique_id(-1);
+ typed_path_pb.set_name("a");
+ typed_path_pb.set_type("INT");
+ typed_path_pb.set_is_nullable(true);
+ typed_path_pb.set_pattern_type(PatternTypePB::MATCH_NAME);
+ TabletColumn typed_path;
+ typed_path.init_from_pb(typed_path_pb);
+ schema->mutable_column_by_uid(1).add_sub_column(typed_path);
+ return schema;
+ }
+
// Inserts one root-scalar JSON object string into a block's variant
column.
static void insert_variant_json(Block& block, size_t variant_pos,
std::string_view json) {
auto* variant = assert_cast<ColumnVariantV2*>(
@@ -500,6 +532,201 @@ TEST_F(VariantRowStoreTest, RowStorePreservesVariantV2) {
EXPECT_EQ(stored_variant.find(R"("flag":1)"), std::string::npos) <<
stored_variant;
}
+// UPSERT publish-conflict rewrites read the complete current row. Keep that
path on row-store
+// instead of opening every physical column, while fixed updates remain free
to choose a narrow
+// physical projection.
+TEST_F(VariantRowStoreTest, UpsertPublishConflictUsesRowStore) {
+ auto schema = create_variant_row_store_schema();
+ TabletSharedPtr tablet;
+ auto current_rowset = write_rowset_block(
+ schema, 8201, 2,
+ [&](Block& block) {
+ int32_t key = 1;
+ int8_t delete_sign = 0;
+ block.get_by_position(0).column->assert_mutable()->insert_data(
+ reinterpret_cast<const char*>(&key), sizeof(key));
+ insert_variant_json(block, 1, R"({"flag":true})");
+ block.get_by_position(2).column->assert_mutable()->insert_data(
+ reinterpret_cast<const char*>(&delete_sign),
sizeof(delete_sign));
+
block.get_by_position(3).column->assert_mutable()->insert_default();
+ },
+ &tablet);
+
+ Block persisted;
+ ASSERT_TRUE(read_rowset(current_rowset, schema, &persisted).ok());
+ EXPECT_NE(variant_row_json(persisted, 1, 0).find(R"("flag":true)"),
std::string::npos);
+ const auto& persisted_row_store =
+ assert_cast<const
ColumnString&>(*persisted.get_by_position(3).column);
+ Block decoded_before_rewrite =
+ decode_row_store_cell(schema, persisted_row_store.get_data_at(0));
+ EXPECT_NE(variant_row_json(decoded_before_rewrite, 1,
0).find(R"("flag":true)"),
+ std::string::npos);
+
+ auto partial_update_info = std::make_shared<PartialUpdateInfo>();
+ ASSERT_TRUE(partial_update_info
+ ->init(kTabletId, /*txn_id=*/1, *schema,
UniqueKeyUpdateModePB::UPSERT,
+ PartialUpdateNewRowPolicyPB::APPEND, {},
/*is_strict_mode=*/false,
+ /*timestamp_ms=*/0, /*nano_seconds=*/0, "UTC",
"")
+ .ok());
+ partial_update_info->update_cids.resize(schema->num_columns());
+ std::iota(partial_update_info->update_cids.begin(),
partial_update_info->update_cids.end(), 0);
+
+ FixedReadPlan read_plan_update;
+ read_plan_update.prepare_to_read(
+ RowLocation {current_rowset->rowset_id(), /*segment_id=*/0,
/*row_id=*/0},
+ /*dst_pos=*/0);
+ FixedReadPlan empty_historical_plan;
+ std::map<RowsetId, RowsetSharedPtr> rowsets {{current_rowset->rowset_id(),
current_rowset}};
+
+ int row_store_reads = 0;
+ int batch_column_reads = 0;
+ auto* sync_point = SyncPoint::get_instance();
+ SyncPoint::CallbackGuard row_store_read_guard;
+ SyncPoint::CallbackGuard batch_read_guard;
+ sync_point->set_call_back(
+ "BaseTablet::fetch_value_through_row_column",
+ [&](auto&& args) {
+ auto* rowset = try_any_cast<BetaRowset*>(args[0]);
+ if (rowset->rowset_id() == current_rowset->rowset_id()) {
+ ++row_store_reads;
+ }
+ },
+ &row_store_read_guard);
+ sync_point->set_call_back(
+ "BaseTablet::fetch_values_by_rowids",
+ [&](auto&& args) {
+ auto* rowset = try_any_cast<BetaRowset*>(args[0]);
+ if (rowset->rowset_id() == current_rowset->rowset_id()) {
+ ++batch_column_reads;
+ }
+ },
+ &batch_read_guard);
+ sync_point->enable_processing();
+
+ auto rebuilt = schema->create_storage_block();
+ auto rebuild_status = BaseTablet::generate_new_block_for_partial_update(
+ schema, partial_update_info.get(), empty_historical_plan,
read_plan_update, rowsets,
+ &rebuilt);
+ sync_point->disable_processing();
+
+ ASSERT_TRUE(rebuild_status.ok()) << rebuild_status;
+ EXPECT_EQ(row_store_reads, 1);
+ EXPECT_EQ(batch_column_reads, 0);
+ EXPECT_NE(variant_row_json(rebuilt, 1, 0).find(R"("flag":true)"),
std::string::npos);
+
+ RowsetWriterContext transient_context = direct_rwc(schema);
+ transient_context.partial_update_info = partial_update_info;
+ transient_context.is_transient_rowset_writer = true;
+ auto chain = build_transform_chain(transient_context);
+ EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view> {"Validate",
"RowStoreFill"}));
+ auto transform_context = exec_ctx(schema, &transient_context);
+ ASSERT_TRUE(chain.apply(transform_context, &rebuilt).ok());
+ ASSERT_TRUE(materialize_derived_columns(transform_context.derived_column,
&rebuilt).ok());
+
+ EXPECT_NE(variant_row_json(rebuilt, 1, 0).find(R"("flag":true)"),
std::string::npos);
+ const auto& rebuilt_row_store =
+ assert_cast<const
ColumnString&>(*rebuilt.get_by_position(3).column);
+ Block decoded_after_rewrite = decode_row_store_cell(schema,
rebuilt_row_store.get_data_at(0));
+ const std::string stored_variant = variant_row_json(decoded_after_rewrite,
1, 0);
+ EXPECT_NE(stored_variant.find(R"("flag":true)"), std::string::npos) <<
stored_variant;
+ EXPECT_EQ(stored_variant.find(R"("flag":1)"), std::string::npos) <<
stored_variant;
+}
+
+// Fixed updates normally read their narrow current projection from physical
columns during
+// publish-conflict reconstruction. Variant is the exception: typed paths are
coerced by the
+// column writer after RowStoreFill, so the physical value can no longer
reproduce the row-store
+// representation written by the original update.
+TEST_F(VariantRowStoreTest, FixedPublishConflictPreservesTypedVariantRowStore)
{
+ auto schema = create_typed_variant_pu_row_store_schema();
+ TabletSharedPtr tablet;
+ auto current_rowset = write_rowset_block(
+ schema, 8301, 3,
+ [&](Block& block) {
+ int32_t key = 1;
+ int8_t delete_sign = 0;
+ int32_t vv = 100;
+ block.get_by_position(0).column->assert_mutable()->insert_data(
+ reinterpret_cast<const char*>(&key), sizeof(key));
+ insert_variant_json(block, 1, R"({"a":"001"})");
+ block.get_by_position(2).column->assert_mutable()->insert_data(
+ reinterpret_cast<const char*>(&delete_sign),
sizeof(delete_sign));
+ block.get_by_position(3).column->assert_mutable()->insert_data(
+ reinterpret_cast<const char*>(&vv), sizeof(vv));
+
block.get_by_position(4).column->assert_mutable()->insert_default();
+ },
+ &tablet);
+ TabletSharedPtr unused_historical_tablet;
+ auto historical_rowset = write_rowset_block(
+ schema, 8302, 2,
+ [&](Block& block) {
+ int32_t key = 1;
+ int8_t delete_sign = 0;
+ int32_t vv = 7;
+ block.get_by_position(0).column->assert_mutable()->insert_data(
+ reinterpret_cast<const char*>(&key), sizeof(key));
+ insert_variant_json(block, 1, R"({"a":9})");
+ block.get_by_position(2).column->assert_mutable()->insert_data(
+ reinterpret_cast<const char*>(&delete_sign),
sizeof(delete_sign));
+ block.get_by_position(3).column->assert_mutable()->insert_data(
+ reinterpret_cast<const char*>(&vv), sizeof(vv));
+
block.get_by_position(4).column->assert_mutable()->insert_default();
+ },
+ &unused_historical_tablet);
+
+ Block persisted;
+ ASSERT_TRUE(read_rowset(current_rowset, schema, &persisted).ok());
+ EXPECT_NE(variant_row_json(persisted, 1, 0).find(R"("a":1)"),
std::string::npos);
+ const auto& persisted_row_store =
+ assert_cast<const
ColumnString&>(*persisted.get_by_position(4).column);
+ Block decoded_before_rewrite =
+ decode_row_store_cell(schema, persisted_row_store.get_data_at(0));
+ EXPECT_NE(variant_row_json(decoded_before_rewrite, 1,
0).find(R"("a":"001")"),
+ std::string::npos);
+
+ auto partial_update_info = std::make_shared<PartialUpdateInfo>();
+ ASSERT_TRUE(partial_update_info
+ ->init(kTabletId, /*txn_id=*/1, *schema,
+ UniqueKeyUpdateModePB::UPDATE_FIXED_COLUMNS,
+ PartialUpdateNewRowPolicyPB::APPEND, {"k", "v"},
+ /*is_strict_mode=*/false, /*timestamp_ms=*/0,
+ /*nano_seconds=*/0, "UTC", "")
+ .ok());
+
+ FixedReadPlan read_plan_update;
+ read_plan_update.prepare_to_read(
+ RowLocation {current_rowset->rowset_id(), /*segment_id=*/0,
/*row_id=*/0},
+ /*dst_pos=*/0);
+ FixedReadPlan read_plan_historical;
+ read_plan_historical.prepare_to_read(
+ RowLocation {historical_rowset->rowset_id(), /*segment_id=*/0,
/*row_id=*/0},
+ /*dst_pos=*/0);
+ std::map<RowsetId, RowsetSharedPtr> rowsets {
+ {current_rowset->rowset_id(), current_rowset},
+ {historical_rowset->rowset_id(), historical_rowset}};
+
+ auto rebuilt = schema->create_storage_block();
+ ASSERT_TRUE(BaseTablet::generate_new_block_for_partial_update(
+ schema, partial_update_info.get(),
read_plan_historical, read_plan_update,
+ rowsets, &rebuilt)
+ .ok());
+ EXPECT_NE(variant_row_json(rebuilt, 1, 0).find(R"("a":"001")"),
std::string::npos);
+
+ RowsetWriterContext transient_context = direct_rwc(schema);
+ transient_context.partial_update_info = partial_update_info;
+ transient_context.is_transient_rowset_writer = true;
+ auto chain = build_transform_chain(transient_context);
+ auto transform_context = exec_ctx(schema, &transient_context);
+ ASSERT_TRUE(chain.apply(transform_context, &rebuilt).ok());
+ ASSERT_TRUE(materialize_derived_columns(transform_context.derived_column,
&rebuilt).ok());
+
+ const auto& rebuilt_row_store =
+ assert_cast<const
ColumnString&>(*rebuilt.get_by_position(4).column);
+ Block decoded_after_rewrite = decode_row_store_cell(schema,
rebuilt_row_store.get_data_at(0));
+ const std::string stored_variant = variant_row_json(decoded_after_rewrite,
1, 0);
+ EXPECT_NE(stored_variant.find(R"("a":"001")"), std::string::npos) <<
stored_variant;
+ EXPECT_EQ(stored_variant.find(R"("a":1)"), std::string::npos) <<
stored_variant;
+}
+
// Drive the registered generator directly as the vertical writer does -- a
// fresh clone_empty() dst per batch, max_bytes huge, batch_rows = 2. Over 5
// rows this yields 2,2,1 and walks pos 0->2->4->5, and the concatenation
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]