Skip to content

Commit e13ba03

Browse files
committed
perf(scan): reuse immutable snapshot read views
1 parent d0c1581 commit e13ba03

21 files changed

Lines changed: 1048 additions & 47 deletions

‎include/paimon/scan_context.h‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ScanFilter;
3737
class Executor;
3838
class MemoryPool;
3939
class Predicate;
40+
class SnapshotReadView;
4041

4142
/// `ScanContext` is some configuration for table scan operations.
4243
///
@@ -56,6 +57,18 @@ class PAIMON_EXPORT ScanContext {
5657
const std::map<std::string, std::string>& options,
5758
const std::shared_ptr<Cache>& cache);
5859

60+
ScanContext(const std::string& path, bool is_streaming_mode, std::optional<int32_t> limit,
61+
const std::shared_ptr<ScanFilter>& scan_filter,
62+
const std::shared_ptr<GlobalIndexResult>& global_index_result,
63+
const std::shared_ptr<RealtimeContext>& realtime_context,
64+
const std::shared_ptr<MemoryPool>& memory_pool,
65+
const std::shared_ptr<Executor>& executor,
66+
const std::shared_ptr<FileSystem>& specific_file_system,
67+
const std::optional<std::string>& table_schema,
68+
const std::map<std::string, std::string>& options,
69+
const std::shared_ptr<Cache>& cache,
70+
const std::shared_ptr<const SnapshotReadView>& snapshot_read_view);
71+
5972
~ScanContext();
6073

6174
const std::string& GetPath() const {
@@ -105,6 +118,10 @@ class PAIMON_EXPORT ScanContext {
105118
return cache_;
106119
}
107120

121+
std::shared_ptr<const SnapshotReadView> GetSnapshotReadView() const {
122+
return snapshot_read_view_;
123+
}
124+
108125
private:
109126
std::string path_;
110127
bool is_streaming_mode_;
@@ -118,6 +135,7 @@ class PAIMON_EXPORT ScanContext {
118135
std::optional<std::string> table_schema_;
119136
std::map<std::string, std::string> options_;
120137
std::shared_ptr<Cache> cache_;
138+
std::shared_ptr<const SnapshotReadView> snapshot_read_view_;
121139
};
122140

123141
/// Filter configuration for table scan operations
@@ -220,6 +238,15 @@ class PAIMON_EXPORT ScanContextBuilder {
220238
/// @return Reference to this builder for method chaining.
221239
ScanContextBuilder& WithCache(const std::shared_ptr<Cache>& cache);
222240

241+
/// Reuse the immutable parsed snapshot metadata from an earlier batch scan plan.
242+
///
243+
/// The view must belong to the same normalized table path and branch. Snapshot read views are
244+
/// supported for non-streaming latest-snapshot scans.
245+
/// @param snapshot_read_view A read view returned by `Plan::GetSnapshotReadView()`.
246+
/// @return Reference to this builder for method chaining.
247+
ScanContextBuilder& WithSnapshotReadView(
248+
const std::shared_ptr<const SnapshotReadView>& snapshot_read_view);
249+
223250
/// Build and return a `ScanContext` instance with input validation.
224251
/// @return Result containing the constructed `ScanContext` or an error status.
225252
Result<std::unique_ptr<ScanContext>> Finish();

‎include/paimon/table/source/plan.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <optional>
2424
#include <vector>
2525

26+
#include "paimon/table/source/snapshot_read_view.h"
2627
#include "paimon/table/source/split.h"
2728

2829
namespace paimon {
@@ -34,5 +35,13 @@ class PAIMON_EXPORT Plan {
3435
virtual const std::vector<std::shared_ptr<Split>>& Splits() const = 0;
3536
/// Snapshot id of this plan, return `std::nullopt` if the table is empty.
3637
virtual std::optional<int64_t> SnapshotId() const = 0;
38+
/// Immutable snapshot metadata used to build this plan.
39+
///
40+
/// The returned view can be injected into a later batch scan to reuse the already resolved and
41+
/// parsed snapshot. Only non-streaming, non-real-time latest-snapshot plans publish a reusable
42+
/// view; other plans return `nullptr`.
43+
virtual std::shared_ptr<const SnapshotReadView> GetSnapshotReadView() const {
44+
return nullptr;
45+
}
3746
};
3847
} // namespace paimon
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
#include <cstdint>
23+
#include <optional>
24+
#include <string>
25+
26+
#include "paimon/visibility.h"
27+
28+
namespace paimon {
29+
30+
class SnapshotReadViewImpl;
31+
32+
/// An immutable, process-local view of the snapshot and table-schema metadata used to create a
33+
/// scan plan.
34+
///
35+
/// A view is bound to one base table path and branch. Pass a view returned by `Plan` to
36+
/// `ScanContextBuilder::WithSnapshotReadView()` to plan another batch scan against the same
37+
/// parsed planning metadata without resolving the latest snapshot or schema again. Views are
38+
/// published and accepted only for non-streaming, non-real-time latest-snapshot scans. A missing
39+
/// snapshot id represents an empty table at the time the view was created. The base path identity
40+
/// is also used by supported system-table scans such as `$ro`, because they consume the same
41+
/// snapshot and schema metadata.
42+
class PAIMON_EXPORT SnapshotReadView {
43+
public:
44+
virtual ~SnapshotReadView() = default;
45+
46+
/// Normalized base table path this view belongs to, without a system-table suffix.
47+
virtual const std::string& TablePath() const = 0;
48+
49+
/// Normalized branch this view belongs to.
50+
virtual const std::string& Branch() const = 0;
51+
52+
/// Snapshot id captured by this view, or `std::nullopt` if the table was empty.
53+
virtual std::optional<int64_t> SnapshotId() const = 0;
54+
55+
private:
56+
SnapshotReadView() = default;
57+
58+
friend class SnapshotReadViewImpl;
59+
};
60+
61+
} // namespace paimon

‎src/paimon/core/operation/scan_context.cpp‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ ScanContext::ScanContext(const std::string& path, bool is_streaming_mode,
3939
const std::optional<std::string>& table_schema,
4040
const std::map<std::string, std::string>& options,
4141
const std::shared_ptr<Cache>& cache)
42+
: ScanContext(path, is_streaming_mode, limit, scan_filter, global_index_result,
43+
realtime_context, memory_pool, executor, specific_file_system, table_schema,
44+
options, cache, nullptr) {}
45+
46+
ScanContext::ScanContext(const std::string& path, bool is_streaming_mode,
47+
std::optional<int32_t> limit,
48+
const std::shared_ptr<ScanFilter>& scan_filter,
49+
const std::shared_ptr<GlobalIndexResult>& global_index_result,
50+
const std::shared_ptr<RealtimeContext>& realtime_context,
51+
const std::shared_ptr<MemoryPool>& memory_pool,
52+
const std::shared_ptr<Executor>& executor,
53+
const std::shared_ptr<FileSystem>& specific_file_system,
54+
const std::optional<std::string>& table_schema,
55+
const std::map<std::string, std::string>& options,
56+
const std::shared_ptr<Cache>& cache,
57+
const std::shared_ptr<const SnapshotReadView>& snapshot_read_view)
4258
: path_(path),
4359
is_streaming_mode_(is_streaming_mode),
4460
limit_(limit),
@@ -50,7 +66,8 @@ ScanContext::ScanContext(const std::string& path, bool is_streaming_mode,
5066
specific_file_system_(specific_file_system),
5167
table_schema_(table_schema),
5268
options_(options),
53-
cache_(cache) {}
69+
cache_(cache),
70+
snapshot_read_view_(snapshot_read_view) {}
5471

5572
ScanContext::~ScanContext() = default;
5673

@@ -72,6 +89,7 @@ class ScanContextBuilder::Impl {
7289
table_schema_ = std::nullopt;
7390
options_.clear();
7491
cache_.reset();
92+
snapshot_read_view_.reset();
7593
}
7694

7795
private:
@@ -89,6 +107,7 @@ class ScanContextBuilder::Impl {
89107
std::optional<std::string> table_schema_;
90108
std::map<std::string, std::string> options_;
91109
std::shared_ptr<Cache> cache_;
110+
std::shared_ptr<const SnapshotReadView> snapshot_read_view_;
92111
};
93112

94113
ScanContextBuilder::ScanContextBuilder(const std::string& path)
@@ -173,6 +192,12 @@ ScanContextBuilder& ScanContextBuilder::WithCache(const std::shared_ptr<Cache>&
173192
return *this;
174193
}
175194

195+
ScanContextBuilder& ScanContextBuilder::WithSnapshotReadView(
196+
const std::shared_ptr<const SnapshotReadView>& snapshot_read_view) {
197+
impl_->snapshot_read_view_ = snapshot_read_view;
198+
return *this;
199+
}
200+
176201
Result<std::unique_ptr<ScanContext>> ScanContextBuilder::Finish() {
177202
PAIMON_ASSIGN_OR_RAISE(impl_->path_, PathUtil::NormalizePath(impl_->path_));
178203
if (impl_->path_.empty()) {
@@ -184,7 +209,7 @@ Result<std::unique_ptr<ScanContext>> ScanContextBuilder::Finish() {
184209
impl_->bucket_filter_),
185210
impl_->global_index_result_, impl_->realtime_context_, impl_->memory_pool_,
186211
impl_->executor_, impl_->specific_file_system_, impl_->table_schema_, impl_->options_,
187-
impl_->cache_);
212+
impl_->cache_, impl_->snapshot_read_view_);
188213
impl_->Reset();
189214
return ctx;
190215
}

‎src/paimon/core/table/source/data_evolution_batch_scan.cpp‎

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "paimon/core/global_index/global_index_scan_impl.h"
2626
#include "paimon/core/global_index/indexed_split_impl.h"
2727
#include "paimon/core/table/source/data_split_impl.h"
28+
#include "paimon/core/table/source/snapshot_read_view_impl.h"
2829
#include "paimon/core/utils/snapshot_manager.h"
2930
#include "paimon/global_index/bitmap_global_index_result.h"
3031

@@ -44,6 +45,11 @@ DataEvolutionBatchScan::DataEvolutionBatchScan(
4445
executor_(executor) {}
4546

4647
Result<std::shared_ptr<Plan>> DataEvolutionBatchScan::CreatePlan() {
48+
std::shared_ptr<const SnapshotReadView> snapshot_read_view =
49+
snapshot_reader_->GetSnapshotReadView();
50+
if (snapshot_read_view && !snapshot_read_view->SnapshotId()) {
51+
return PlanImpl::EmptyPlan(snapshot_read_view);
52+
}
4753
std::optional<std::vector<Range>> row_ranges;
4854
std::shared_ptr<GlobalIndexResult> final_global_index_result = global_index_result_;
4955
if (!final_global_index_result) {
@@ -52,14 +58,22 @@ Result<std::shared_ptr<Plan>> DataEvolutionBatchScan::CreatePlan() {
5258
final_global_index_result = index_result;
5359
PAIMON_ASSIGN_OR_RAISE(row_ranges, index_result->ToRanges());
5460
}
61+
// EvalGlobalIndex captures the latest snapshot before consulting its index. Refresh the
62+
// local copy so both negative and positive results publish and reuse that exact view.
63+
snapshot_read_view = snapshot_reader_->GetSnapshotReadView();
5564
} else {
5665
PAIMON_ASSIGN_OR_RAISE(row_ranges, final_global_index_result->ToRanges());
5766
}
5867
if (!row_ranges) {
5968
return batch_scan_->CreatePlan();
6069
}
6170
if (row_ranges.value().empty()) {
62-
return PlanImpl::EmptyPlan();
71+
if (snapshot_read_view && snapshot_read_view->SnapshotId()) {
72+
return std::make_shared<PlanImpl>(snapshot_read_view->SnapshotId(),
73+
std::vector<std::shared_ptr<Split>>(),
74+
snapshot_read_view);
75+
}
76+
return PlanImpl::EmptyPlan(snapshot_read_view);
6377
}
6478
PAIMON_ASSIGN_OR_RAISE(RowRangeIndex row_range_index,
6579
RowRangeIndex::Create(row_ranges.value()));
@@ -133,7 +147,8 @@ Result<std::shared_ptr<Plan>> DataEvolutionBatchScan::WrapToIndexedSplits(
133147
}
134148
indexed_splits.push_back(std::make_shared<IndexedSplitImpl>(data_split, expected, scores));
135149
}
136-
return std::make_shared<PlanImpl>(data_plan->SnapshotId(), indexed_splits);
150+
return std::make_shared<PlanImpl>(data_plan->SnapshotId(), indexed_splits,
151+
data_plan->GetSnapshotReadView());
137152
}
138153

139154
Result<std::shared_ptr<GlobalIndexResult>> DataEvolutionBatchScan::EvalGlobalIndex() const {
@@ -145,24 +160,40 @@ Result<std::shared_ptr<GlobalIndexResult>> DataEvolutionBatchScan::EvalGlobalInd
145160
return std::shared_ptr<GlobalIndexResult>(nullptr);
146161
}
147162
auto partition_filter = batch_scan_->GetPartitionPredicate();
148-
// TODO(lisizhuo.lsz): support time travel
149-
std::optional<Snapshot> snapshot;
150-
const std::shared_ptr<SnapshotManager>& snapshot_manager =
151-
snapshot_reader_->GetSnapshotManager();
152-
if (const std::optional<int64_t>& snapshot_id = core_options_.GetScanSnapshotId()) {
153-
PAIMON_ASSIGN_OR_RAISE(Snapshot loaded_snapshot,
154-
snapshot_manager->LoadSnapshot(snapshot_id.value()));
155-
snapshot = std::move(loaded_snapshot);
163+
StartupMode startup_mode = core_options_.GetStartupMode();
164+
std::shared_ptr<const Snapshot> snapshot;
165+
if (!(startup_mode == StartupMode::LatestFull() || startup_mode == StartupMode::Latest())) {
166+
// Snapshot read views deliberately support latest scans only. Preserve main's non-latest
167+
// planning path, including an explicitly selected snapshot and the caller-owned context.
168+
// TODO(lisizhuo.lsz): support tag/timestamp time travel.
169+
std::optional<Snapshot> loaded_snapshot;
170+
const std::shared_ptr<SnapshotManager>& snapshot_manager =
171+
snapshot_reader_->GetSnapshotManager();
172+
if (const std::optional<int64_t>& snapshot_id = core_options_.GetScanSnapshotId()) {
173+
PAIMON_ASSIGN_OR_RAISE(Snapshot selected_snapshot,
174+
snapshot_manager->LoadSnapshot(snapshot_id.value()));
175+
loaded_snapshot = std::move(selected_snapshot);
176+
} else {
177+
PAIMON_ASSIGN_OR_RAISE(loaded_snapshot, snapshot_manager->LatestSnapshot());
178+
}
179+
if (!loaded_snapshot) {
180+
return Status::Invalid("not found latest snapshot");
181+
}
182+
snapshot = std::make_shared<const Snapshot>(std::move(loaded_snapshot).value());
156183
} else {
157-
PAIMON_ASSIGN_OR_RAISE(snapshot, snapshot_manager->LatestSnapshot());
158-
}
159-
if (!snapshot) {
160-
return Status::Invalid("not found latest snapshot");
184+
// Capture the latest snapshot through the shared SnapshotReader before evaluating the
185+
// index. The subsequent data scan then consumes the same immutable view, avoiding both a
186+
// second latest-snapshot lookup and a row-id/snapshot race.
187+
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<const SnapshotReadView> snapshot_read_view,
188+
snapshot_reader_->CaptureLatestSnapshotReadView());
189+
PAIMON_ASSIGN_OR_RAISE(snapshot, SnapshotReadViewImpl::GetSnapshot(snapshot_read_view));
190+
if (!snapshot) {
191+
return std::shared_ptr<GlobalIndexResult>(nullptr);
192+
}
161193
}
162-
163194
PAIMON_ASSIGN_OR_RAISE(
164195
std::unique_ptr<GlobalIndexScanImpl> index_scan,
165-
GlobalIndexScanImpl::Create(table_path_, table_schema_, snapshot.value(), partition_filter,
196+
GlobalIndexScanImpl::Create(table_path_, table_schema_, *snapshot, partition_filter,
166197
core_options_, executor_, pool_));
167198
return index_scan->Scan(predicate);
168199
}

‎src/paimon/core/table/source/data_table_batch_scan.cpp‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Result<std::shared_ptr<Plan>> DataTableBatchScan::ApplyPushDownLimit(
9090
std::dynamic_pointer_cast<StartingScanner::CurrentSnapshot>(scan_result);
9191
if (!current_scan_result) {
9292
// NoSnapshot
93-
return PlanImpl::EmptyPlan();
93+
return snapshot_reader_->EmptyPlan();
9494
}
9595
if (!CanPushDownLimit()) {
9696
return current_scan_result->GetPlan();
@@ -126,7 +126,9 @@ Result<std::shared_ptr<Plan>> DataTableBatchScan::ApplyPushDownLimit(
126126
"rows.",
127127
limited_data_splits.size(), splits.size(), push_down_limit_.value(),
128128
scanned_row_count);
129-
return std::make_shared<PlanImpl>(snapshot_id, limited_data_splits);
129+
return std::make_shared<PlanImpl>(
130+
snapshot_id, limited_data_splits,
131+
current_scan_result->GetPlan()->GetSnapshotReadView());
130132
}
131133
}
132134
return current_scan_result->GetPlan();

‎src/paimon/core/table/source/plan_impl.cpp‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,13 @@ const std::shared_ptr<Plan> PlanImpl::EmptyPlan() {
2727
return empty_plan;
2828
}
2929

30+
const std::shared_ptr<Plan> PlanImpl::EmptyPlan(
31+
const std::shared_ptr<const SnapshotReadView>& snapshot_read_view) {
32+
if (!snapshot_read_view) {
33+
return EmptyPlan();
34+
}
35+
return std::make_shared<PlanImpl>(std::optional<int64_t>(),
36+
std::vector<std::shared_ptr<Split>>(), snapshot_read_view);
37+
}
38+
3039
} // namespace paimon

‎src/paimon/core/table/source/plan_impl.h‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ class PlanImpl : public Plan {
3333
public:
3434
PlanImpl(const std::optional<int64_t>& snapshot_id,
3535
const std::vector<std::shared_ptr<Split>>& splits)
36-
: snapshot_id_(snapshot_id), splits_(splits) {}
36+
: PlanImpl(snapshot_id, splits, nullptr) {}
37+
38+
PlanImpl(const std::optional<int64_t>& snapshot_id,
39+
const std::vector<std::shared_ptr<Split>>& splits,
40+
const std::shared_ptr<const SnapshotReadView>& snapshot_read_view)
41+
: snapshot_id_(snapshot_id), splits_(splits), snapshot_read_view_(snapshot_read_view) {}
3742

3843
std::optional<int64_t> SnapshotId() const override {
3944
return snapshot_id_;
@@ -43,10 +48,18 @@ class PlanImpl : public Plan {
4348
return splits_;
4449
}
4550

51+
std::shared_ptr<const SnapshotReadView> GetSnapshotReadView() const override {
52+
return snapshot_read_view_;
53+
}
54+
4655
static const std::shared_ptr<Plan> EmptyPlan();
4756

57+
static const std::shared_ptr<Plan> EmptyPlan(
58+
const std::shared_ptr<const SnapshotReadView>& snapshot_read_view);
59+
4860
private:
4961
std::optional<int64_t> snapshot_id_;
5062
std::vector<std::shared_ptr<Split>> splits_;
63+
std::shared_ptr<const SnapshotReadView> snapshot_read_view_;
5164
};
5265
} // namespace paimon

0 commit comments

Comments
 (0)