Skip to content

Commit ded7cfb

Browse files
authored
perf(read): reduce point-lookup metadata I/O (#285)
1 parent a42670e commit ded7cfb

7 files changed

Lines changed: 349 additions & 12 deletions

‎src/paimon/core/utils/snapshot_manager.cpp‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,6 @@ Result<std::optional<int64_t>> SnapshotManager::FindEarliest(
141141
Result<std::optional<int64_t>> SnapshotManager::FindLatest(
142142
const std::string& dir, const std::string& prefix,
143143
const std::function<std::string(int64_t)>& path_func) const {
144-
PAIMON_ASSIGN_OR_RAISE(bool is_exist, fs_->Exists(dir));
145-
if (!is_exist) {
146-
return std::optional<int64_t>();
147-
}
148144
std::optional<int64_t> snapshot_id = ReadHint(LATEST, dir);
149145
if (snapshot_id != std::nullopt && snapshot_id.value() > 0) {
150146
int64_t next_snapshot = snapshot_id.value() + 1;
@@ -155,6 +151,8 @@ Result<std::optional<int64_t>> SnapshotManager::FindLatest(
155151
return snapshot_id;
156152
}
157153
}
154+
// A valid hint needs no parent-directory probe. The listing fallback checks
155+
// directory existence itself, including tables without any snapshots yet.
158156
return FindByListFiles([](int64_t lhs, int64_t rhs) -> int64_t { return std::max(lhs, rhs); },
159157
dir, prefix);
160158
}

‎src/paimon/core/utils/snapshot_manager_test.cpp‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
#include <filesystem>
2222
#include <limits>
23+
#include <vector>
2324

2425
#include "gtest/gtest.h"
2526
#include "paimon/common/utils/path_util.h"
@@ -139,6 +140,33 @@ TEST(SnapshotManagerTest, TestPathNotExist) {
139140
ASSERT_EQ(snapshot, std::nullopt);
140141
}
141142

143+
TEST(SnapshotManagerTest, LatestSnapshotWithStaleMissingOrInvalidHint) {
144+
auto dir = UniqueTestDirectory::Create();
145+
ASSERT_TRUE(dir);
146+
auto fs = std::make_shared<LocalFileSystem>();
147+
SnapshotManager mgr(fs, dir->Str());
148+
ASSERT_OK(fs->Mkdirs(mgr.SnapshotDirectory()));
149+
ASSERT_OK(fs->WriteFile(mgr.SnapshotPath(1), "{}", true));
150+
ASSERT_OK(mgr.CommitLatestHint(1));
151+
152+
ASSERT_OK_AND_ASSIGN(std::optional<int64_t> latest, mgr.LatestSnapshotId());
153+
ASSERT_EQ(latest, 1);
154+
155+
// A commit can publish its snapshot before updating the hint.
156+
ASSERT_OK(fs->WriteFile(mgr.SnapshotPath(2), "{}", true));
157+
ASSERT_OK_AND_ASSIGN(latest, mgr.LatestSnapshotId());
158+
ASSERT_EQ(latest, 2);
159+
160+
const std::string hint_path = PathUtil::JoinPath(mgr.SnapshotDirectory(), "LATEST");
161+
ASSERT_OK(fs->Delete(hint_path));
162+
ASSERT_OK_AND_ASSIGN(latest, mgr.LatestSnapshotId());
163+
ASSERT_EQ(latest, 2);
164+
165+
ASSERT_OK(fs->WriteFile(hint_path, "invalid", true));
166+
ASSERT_OK_AND_ASSIGN(latest, mgr.LatestSnapshotId());
167+
ASSERT_EQ(latest, 2);
168+
}
169+
142170
TEST(SnapshotManagerTest, TestEarlierOrEqualTimeMillisExactMatch) {
143171
std::string test_data_path = paimon::test::GetDataDir() + "/orc/append_09.db/append_09";
144172
auto file_system = std::make_shared<LocalFileSystem>();

‎src/paimon/format/parquet/file_reader_wrapper.cpp‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
#include "fmt/format.h"
3030
#include "paimon/common/utils/arrow/arrow_utils.h"
3131
#include "paimon/common/utils/math.h"
32+
#include "paimon/common/utils/scope_guard.h"
3233
#include "paimon/format/parquet/column_index_filter.h"
3334
#include "paimon/format/parquet/page_filtered_row_group_reader.h"
3435
#include "paimon/format/parquet/parquet_format_defs.h"
3536
#include "paimon/macros.h"
37+
#include "paimon/predicate/predicate_utils.h"
3638
#include "parquet/arrow/reader.h"
3739
#include "parquet/arrow/schema.h"
3840
#include "parquet/file_reader.h"
@@ -602,8 +604,35 @@ Result<RowRanges> FileReaderWrapper::CalculateFilteredRowRanges(
602604
return RowRanges::CreateSingle(row_count);
603605
}
604606

607+
auto page_index_reader = GetPageIndexReader();
608+
if (!page_index_reader) {
609+
return RowRanges::CreateSingle(row_count);
610+
}
611+
std::set<std::string> field_names;
612+
PAIMON_RETURN_NOT_OK(PredicateUtils::GetAllNames(predicate, &field_names));
613+
std::vector<int32_t> predicate_columns;
614+
for (const auto& name : field_names) {
615+
auto it = column_name_to_index.find(name);
616+
if (it == column_name_to_index.end()) {
617+
return Status::Invalid(
618+
fmt::format("column '{}' not found in column_name_to_index", name));
619+
}
620+
predicate_columns.push_back(it->second);
621+
}
622+
if (predicate_columns.empty()) {
623+
return RowRanges::CreateSingle(row_count);
624+
}
625+
626+
// Arrow otherwise reads the column-index envelope of every column, including
627+
// potentially large min/max values in payload columns not used by the predicate.
628+
const std::vector<int32_t> row_groups = {row_group_index};
629+
ScopeGuard clear_hint([&]() { page_index_reader->WillNotNeed(row_groups); });
630+
page_index_reader->WillNeed(row_groups, predicate_columns,
631+
{/*column_index=*/true, /*offset_index=*/true});
632+
// Keep this restricted reader separate: projected payload columns still need
633+
// their offset indexes when the data reader is initialized later.
605634
return ColumnIndexFilter::CalculateRowRanges(predicate,
606-
GetRowGroupPageIndexReader(row_group_index),
635+
page_index_reader->RowGroup(row_group_index),
607636
column_name_to_index, row_count);
608637
}
609638
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("FileReaderWrapper::CalculateFilteredRowRanges")

‎src/paimon/format/parquet/file_reader_wrapper_test.cpp‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,14 @@
4646
#include "paimon/fs/file_system.h"
4747
#include "paimon/fs/local/local_file_system.h"
4848
#include "paimon/memory/memory_pool.h"
49+
#include "paimon/predicate/literal.h"
50+
#include "paimon/predicate/predicate_builder.h"
4951
#include "paimon/record_batch.h"
5052
#include "paimon/testing/utils/testharness.h"
5153
#include "parquet/arrow/reader.h"
54+
#include "parquet/file_reader.h"
55+
#include "parquet/metadata.h"
56+
#include "parquet/page_index.h"
5257
#include "parquet/properties.h"
5358

5459
namespace arrow {
@@ -273,6 +278,39 @@ TEST_F(FileReaderWrapperTest, NullFileReader) {
273278
"file reader wrapper create failed. file reader is nullptr");
274279
}
275280

281+
TEST_F(FileReaderWrapperTest, PredicateReadsOnlyItsPageIndexesAndKeepsPayloadReadable) {
282+
std::string file_path = PathUtil::JoinPath(dir_->Str(), "predicate-index.parquet");
283+
PrepareParquetFile(file_path, /*row_count=*/1000, /*enable_page_index=*/true);
284+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input, fs_->Open(file_path));
285+
auto tracking_input = std::make_shared<ReadTrackingInputStream>(input);
286+
ASSERT_OK_AND_ASSIGN(auto reader, PrepareReaderWrapperOnStream(tracking_input));
287+
auto metadata = reader->GetFileReader()->parquet_reader()->metadata();
288+
auto index_ranges =
289+
::parquet::PageIndexReader::DeterminePageIndexRangesInRowGroup(*metadata->RowGroup(0), {1});
290+
ASSERT_TRUE(index_ranges.column_index.has_value());
291+
ASSERT_TRUE(index_ranges.offset_index.has_value());
292+
int64_t bytes_before = tracking_input->GetPositionalReadBytes();
293+
auto predicate = PredicateBuilder::Equal(1, "col2", FieldType::INT, Literal(25));
294+
ASSERT_OK_AND_ASSIGN(auto ranges, reader->CalculateFilteredRowRanges(
295+
0, predicate, {{"col1", 0}, {"col2", 1}, {"col3", 2}}));
296+
ASSERT_EQ(10, ranges.RowCount());
297+
ASSERT_EQ(index_ranges.column_index->length + index_ranges.offset_index->length,
298+
tracking_input->GetPositionalReadBytes() - bytes_before);
299+
300+
// The predicate's read hint must not restrict subsequent payload offset-index reads.
301+
ASSERT_OK(reader->PrepareForReading(
302+
{TargetRowGroup(/*rg_index=*/0, /*is_partially_matched=*/true, ranges)}, {0, 1, 2}));
303+
ASSERT_OK_AND_ASSIGN(auto batch, reader->Next());
304+
ASSERT_TRUE(batch);
305+
auto expected = PrepareArray(PrepareArrowSchema().second, /*record_batch_size=*/10,
306+
/*offset=*/20);
307+
auto expected_batch = arrow::RecordBatch::FromStructArray(expected);
308+
ASSERT_TRUE(expected_batch.ok());
309+
ASSERT_TRUE(batch->Equals(*expected_batch.ValueOrDie()));
310+
ASSERT_OK_AND_ASSIGN(auto end, reader->Next());
311+
ASSERT_FALSE(end);
312+
}
313+
276314
TEST_F(FileReaderWrapperTest, Simple) {
277315
std::string file_path = PathUtil::JoinPath(dir_->Str(), "test.parquet");
278316
PrepareParquetFile(file_path, /*row_count=*/5500);

‎src/paimon/format/parquet/parquet_file_batch_reader_test.cpp‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,122 @@ TEST_F(ParquetFileBatchReaderTest, TestParquetMetadataCacheBypassesWhenGetUriFai
343343
ASSERT_EQ(0, cache->Size());
344344
}
345345

346+
TEST_F(ParquetFileBatchReaderTest, TestPageIndexBytesSurviveReaderClose) {
347+
WriteArray(file_path_, struct_array_, schema_, /*write_batch_size=*/1,
348+
/*enable_dictionary=*/false, /*max_row_group_length=*/3,
349+
/*max_page_size=*/1);
350+
auto cache = std::make_shared<paimon::test::CountingRoutingCache>(CacheKind::DATA_FILE_FOOTER,
351+
128 * 1024 * 1024);
352+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> raw_input, fs_->Open(file_path_));
353+
ASSERT_OK_AND_ASSIGN(int64_t length, raw_input->Length());
354+
auto raw = std::make_shared<ArrowInputStreamAdapter>(raw_input, length, pool_);
355+
auto raw_reader = ::parquet::ParquetFileReader::Open(raw);
356+
auto metadata = raw_reader->metadata();
357+
int64_t expected_index_reads = 0;
358+
std::weak_ptr<MemoryPool> cached_pool;
359+
for (int32_t round = 0; round < 3; ++round) {
360+
if (round == 2) {
361+
cache->InvalidateAll();
362+
ASSERT_TRUE(cached_pool.expired());
363+
}
364+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input, fs_->Open(file_path_));
365+
std::shared_ptr<MemoryPool> query_pool = GetMemoryPool();
366+
auto stream = std::make_shared<ParquetInputStream>(input, length, pool_, query_pool, cache,
367+
file_path_);
368+
stream->SetPageIndexRanges(*metadata);
369+
for (int32_t rg = 0; rg < metadata->num_row_groups(); ++rg) {
370+
auto ranges = ::parquet::PageIndexReader::DeterminePageIndexRangesInRowGroup(
371+
*metadata->RowGroup(rg), {});
372+
for (const auto& range : {ranges.column_index, ranges.offset_index}) {
373+
ASSERT_TRUE(range.has_value());
374+
auto expected = raw->ReadAt(range->offset, range->length);
375+
ASSERT_TRUE(expected.ok()) << expected.status().ToString();
376+
auto actual = stream->ReadAt(range->offset, range->length);
377+
ASSERT_TRUE(actual.ok()) << actual.status().ToString();
378+
ASSERT_TRUE(actual.ValueOrDie()->Equals(*expected.ValueOrDie()));
379+
if (round != 1) {
380+
++expected_index_reads;
381+
}
382+
}
383+
}
384+
ASSERT_EQ(expected_index_reads, cache->SupplierCallCount());
385+
// Reader 2 serves all indexes from the shared cache without storage IO.
386+
if (round == 1) {
387+
ASSERT_EQ(0, stream->StorageReadBytes()->load());
388+
} else {
389+
ASSERT_GT(stream->StorageReadBytes()->load(), 0);
390+
}
391+
// Ordinary file bytes must still use storage, even on a cache hit round.
392+
uint64_t bytes_before = stream->StorageReadBytes()->load();
393+
auto magic = stream->ReadAt(0, 4);
394+
ASSERT_TRUE(magic.ok()) << magic.status().ToString();
395+
ASSERT_EQ("PAR1", magic.ValueOrDie()->ToString());
396+
ASSERT_EQ(bytes_before + 4, stream->StorageReadBytes()->load());
397+
ASSERT_EQ(expected_index_reads, cache->SupplierCallCount());
398+
ASSERT_TRUE(stream->Close().ok());
399+
if (round != 1) {
400+
cached_pool = query_pool;
401+
}
402+
stream.reset();
403+
query_pool.reset();
404+
ASSERT_FALSE(cached_pool.expired());
405+
}
406+
cache->InvalidateAll();
407+
ASSERT_TRUE(cached_pool.expired());
408+
}
409+
410+
TEST_F(ParquetFileBatchReaderTest, TestCachedFooterKeepsAllocatorAliveUntilEviction) {
411+
WriteArray(file_path_, struct_array_, schema_, /*write_batch_size=*/1,
412+
/*enable_dictionary=*/false, /*max_row_group_length=*/3);
413+
auto cache = std::make_shared<paimon::test::CountingRoutingCache>(CacheKind::DATA_FILE_FOOTER,
414+
128 * 1024 * 1024);
415+
std::weak_ptr<MemoryPool> cached_pool;
416+
{
417+
std::shared_ptr<MemoryPool> query_pool = GetMemoryPool();
418+
cached_pool = query_pool;
419+
ParquetReaderBuilder builder({}, 10);
420+
builder.WithMemoryPool(query_pool)->WithCache(cache);
421+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input, fs_->Open(file_path_));
422+
ASSERT_OK_AND_ASSIGN(auto reader, builder.Build(input));
423+
}
424+
ASSERT_FALSE(cached_pool.expired());
425+
cache->InvalidateAll();
426+
ASSERT_TRUE(cached_pool.expired());
427+
}
428+
429+
TEST_F(ParquetFileBatchReaderTest, TestPointReadReusesFooterAndPageIndexes) {
430+
WriteArray(file_path_, struct_array_, schema_, /*write_batch_size=*/1,
431+
/*enable_dictionary=*/false, /*max_row_group_length=*/3,
432+
/*max_page_size=*/1);
433+
auto cache = std::make_shared<paimon::test::CountingRoutingCache>(CacheKind::DATA_FILE_FOOTER,
434+
128 * 1024 * 1024);
435+
auto projection = arrow::schema({schema_->GetFieldByName("f4"), schema_->GetFieldByName("f8")});
436+
auto predicate = PredicateBuilder::Equal(0, "f4", FieldType::INT, Literal(300002));
437+
int64_t cold_reads = 0;
438+
std::shared_ptr<arrow::ChunkedArray> cold_result;
439+
for (int32_t round = 0; round < 2; ++round) {
440+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input, fs_->Open(file_path_));
441+
ParquetReaderBuilder builder({{PARQUET_READ_ENABLE_PAGE_INDEX_FILTER, "true"}}, 10);
442+
builder.WithCache(cache);
443+
ASSERT_OK_AND_ASSIGN(auto reader, builder.Build(input));
444+
ArrowSchema c_schema;
445+
ASSERT_TRUE(arrow::ExportSchema(*projection, &c_schema).ok());
446+
ASSERT_OK(reader->SetReadSchema(&c_schema, predicate, std::nullopt));
447+
ASSERT_OK_AND_ASSIGN(auto result,
448+
paimon::test::ReadResultCollector::CollectResult(reader.get()));
449+
ASSERT_EQ(1, result->length());
450+
if (round == 0) {
451+
cold_reads = cache->SupplierCallCount();
452+
ASSERT_GT(cold_reads, 1); // Footer plus actual page-index ranges.
453+
cold_result = result;
454+
} else {
455+
ASSERT_TRUE(result->Equals(cold_result));
456+
ASSERT_EQ(cold_reads, cache->SupplierCallCount());
457+
ASSERT_GT(cache->GetCount(), cold_reads);
458+
}
459+
}
460+
}
461+
346462
TEST_F(ParquetFileBatchReaderTest, TestReadBinaryWrittenFromBinaryAndLargeBinary) {
347463
auto check_binary_read_result = [&](const std::shared_ptr<arrow::DataType>& write_type,
348464
const std::string& file_name) {

0 commit comments

Comments
 (0)