Skip to content

Commit ea4c852

Browse files
authored
feat: optimize BTree index read with io buffering and boundary skip (#263)
1 parent 95fc626 commit ea4c852

16 files changed

Lines changed: 702 additions & 114 deletions

include/paimon/utils/roaring_bitmap64.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ class PAIMON_EXPORT RoaringBitmap64 {
7474
/// @param x value added to bitmap
7575
void Add(int64_t x);
7676

77+
/// Bulk-insert `n` values into the bitmap.
78+
///
79+
/// Compared to repeatedly calling `Add`, this implementation:
80+
/// 1. Buckets the input values by their high-32 bits in a single pass.
81+
/// 2. Feeds each bucket to the inner 32-bit Roaring's true-batch
82+
/// `addMany(uint32_t*)` path, which performs container-level bulk
83+
/// insertion.
84+
///
85+
/// This avoids the per-value `std::map` lookup of the 64-bit wrapper and
86+
/// the per-value insertion overhead inside the 32-bit array container.
87+
/// Values may be unsorted; ordering is handled internally.
88+
void AddMany(size_t n, const int64_t* values);
89+
7790
/// @param x value added to bitmap
7891
/// @return false if contain x; true if not contain x
7992
bool CheckedAdd(int64_t x);

src/paimon/common/global_index/btree/btree_defs.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ struct BtreeDefs {
4141
static inline const char kBtreeIndexHighPriorityPoolRatio[] =
4242
"btree-index.high-priority-pool-ratio";
4343

44+
/// "btree-index.read-buffer-size" - Optional. Specifies the read buffer size for the B-tree
45+
/// index. This setting can be tuned based on query patterns:
46+
/// - For range queries (e.g., `VisitLessThan`, `VisitGreaterOrEqual`), increasing the buffer
47+
/// size (e.g., to 1MB) may improve I/O bandwidth and sequential read performance.
48+
/// - For point queries (e.g., `VisitEqual`), buffering can introduce negative effects due to
49+
/// read amplification; it is recommended to leave this option unset.
50+
///
51+
/// If specified, read block with `BufferedInputStream`.
52+
static inline const char kBtreeIndexReadBufferSize[] = "btree-index.read-buffer-size";
53+
4454
static inline const char kDefaultBtreeIndexBlockSize[] = "64KB";
4555
static inline const char kDefaultBtreeIndexCompression[] = "none";
4656
static inline const char kDefaultBtreeIndexCacheSize[] = "128MB";

src/paimon/common/global_index/btree/btree_global_index_integration_test.cpp

Lines changed: 115 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020
#include "paimon/common/factories/io_hook.h"
2121
#include "paimon/common/global_index/btree/btree_global_index_writer.h"
2222
#include "paimon/common/global_index/btree/btree_global_indexer.h"
23+
#include "paimon/common/global_index/btree/lazy_filtered_btree_reader.h"
24+
#include "paimon/common/options/memory_size.h"
2325
#include "paimon/common/utils/scope_guard.h"
2426
#include "paimon/data/decimal.h"
2527
#include "paimon/data/timestamp.h"
2628
#include "paimon/fs/file_system.h"
2729
#include "paimon/global_index/bitmap_global_index_result.h"
2830
#include "paimon/global_index/io/global_index_file_reader.h"
2931
#include "paimon/global_index/io/global_index_file_writer.h"
32+
#include "paimon/io/buffered_input_stream.h"
3033
#include "paimon/memory/memory_pool.h"
3134
#include "paimon/predicate/literal.h"
3235
#include "paimon/testing/utils/io_exception_helper.h"
@@ -1506,11 +1509,6 @@ TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadLargeDataWithSmallBlocks) {
15061509
ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
15071510
ASSERT_EQ(metas.size(), 1);
15081511

1509-
auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_, base_path_);
1510-
c_schema = CreateArrowSchema(field);
1511-
ASSERT_OK_AND_ASSIGN(auto reader,
1512-
indexer->CreateReader(c_schema.get(), file_reader, metas, pool_));
1513-
15141512
// Helper lambda: given a predicate on value, collect matching row ids
15151513
auto collect_rows = [total_rows = total_rows](std::function<bool(int32_t)> predicate) {
15161514
std::vector<int64_t> result;
@@ -1529,69 +1527,110 @@ TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadLargeDataWithSmallBlocks) {
15291527
return result;
15301528
};
15311529

1532-
// --- VisitIsNull ---
1533-
{
1534-
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
1535-
CheckResult(result, null_row_ids);
1536-
}
1530+
// Read back with different read-buffer-size configurations:
1531+
// "" -> no buffer (original path)
1532+
// "128" -> very small buffer (smaller than block size 256)
1533+
// "512" -> moderate buffer (larger than block size)
1534+
// "64KB" -> large buffer
1535+
// "1MB" -> very large buffer (likely covers entire file)
1536+
std::vector<std::string> buffer_sizes = {"", "128", "512", "64KB", "1MB"};
1537+
1538+
for (const auto& buf_size : buffer_sizes) {
1539+
std::map<std::string, std::string> read_options = {
1540+
{BtreeDefs::kBtreeIndexBlockSize, "256"}, {BtreeDefs::kBtreeIndexCacheSize, "1024"}};
1541+
if (!buf_size.empty()) {
1542+
read_options[BtreeDefs::kBtreeIndexReadBufferSize] = buf_size;
1543+
}
1544+
ASSERT_OK_AND_ASSIGN(auto read_indexer, BTreeGlobalIndexer::Create(read_options));
15371545

1538-
// --- VisitIsNotNull ---
1539-
{
1540-
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
1541-
CheckResult(result, non_null_row_ids);
1542-
}
1546+
auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_, base_path_);
1547+
c_schema = CreateArrowSchema(field);
1548+
ASSERT_OK_AND_ASSIGN(auto reader,
1549+
read_indexer->CreateReader(c_schema.get(), file_reader, metas, pool_));
1550+
1551+
// check input stream and read buffer size
1552+
{
1553+
auto lazy_reader = std::dynamic_pointer_cast<LazyFilteredBTreeReader>(reader);
1554+
ASSERT_TRUE(lazy_reader);
1555+
ASSERT_OK_AND_ASSIGN(auto tmp_reader, lazy_reader->GetOrCreateReader(metas[0]));
1556+
auto btree_reader = std::dynamic_pointer_cast<BTreeGlobalIndexReader>(tmp_reader);
1557+
ASSERT_TRUE(btree_reader);
1558+
auto input_stream = btree_reader->sst_file_reader_->block_cache_->in_;
1559+
auto buffered_stream = std::dynamic_pointer_cast<BufferedInputStream>(input_stream);
1560+
if (!buf_size.empty()) {
1561+
ASSERT_TRUE(buffered_stream);
1562+
ASSERT_OK_AND_ASSIGN(int32_t expected_buffer_size,
1563+
MemorySize::ParseBytes(buf_size));
1564+
ASSERT_EQ(buffered_stream->buffer_size_, expected_buffer_size);
1565+
} else {
1566+
ASSERT_FALSE(buffered_stream);
1567+
}
1568+
}
15431569

1544-
// --- VisitEqual for value 0 ---
1545-
{
1546-
Literal lit_0(0);
1547-
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_0));
1548-
CheckResult(result, collect_rows([](int32_t v) { return v == 0; }));
1549-
}
1570+
// --- VisitIsNull ---
1571+
{
1572+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
1573+
CheckResult(result, null_row_ids);
1574+
}
15501575

1551-
// --- VisitEqual for a value in the middle ---
1552-
{
1553-
Literal lit_100(100);
1554-
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_100));
1555-
CheckResult(result, collect_rows([](int32_t v) { return v == 100; }));
1556-
}
1576+
// --- VisitIsNotNull ---
1577+
{
1578+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
1579+
CheckResult(result, non_null_row_ids);
1580+
}
15571581

1558-
// --- VisitEqual for non-existent value ---
1559-
{
1560-
Literal lit_neg(static_cast<int32_t>(-1));
1561-
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_neg));
1562-
CheckResult(result, {});
1563-
}
1582+
// --- VisitEqual for value 0 ---
1583+
{
1584+
Literal lit_0(0);
1585+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_0));
1586+
CheckResult(result, collect_rows([](int32_t v) { return v == 0; }));
1587+
}
15641588

1565-
// --- VisitLessThan for value 5 ---
1566-
{
1567-
Literal lit_5(5);
1568-
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_5));
1569-
CheckResult(result, collect_rows([](int32_t v) { return v < 5; }));
1570-
}
1589+
// --- VisitEqual for a value in the middle ---
1590+
{
1591+
Literal lit_100(100);
1592+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_100));
1593+
CheckResult(result, collect_rows([](int32_t v) { return v == 100; }));
1594+
}
15711595

1572-
// --- VisitGreaterOrEqual for a high value near max ---
1573-
{
1574-
int32_t max_val =
1575-
static_cast<int32_t>(collect_rows([](int32_t) { return true; }).size()) / 3;
1576-
int32_t threshold = max_val - 2;
1577-
Literal lit_threshold(threshold);
1578-
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterOrEqual(lit_threshold));
1579-
CheckResult(result, collect_rows([threshold](int32_t v) { return v >= threshold; }));
1580-
}
1596+
// --- VisitEqual for non-existent value ---
1597+
{
1598+
Literal lit_neg(static_cast<int32_t>(-1));
1599+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_neg));
1600+
CheckResult(result, {});
1601+
}
15811602

1582-
// --- VisitIn for scattered values ---
1583-
{
1584-
std::vector<Literal> in_literals = {Literal(0), Literal(500), Literal(10000)};
1585-
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
1586-
CheckResult(result,
1587-
collect_rows([](int32_t v) { return v == 0 || v == 500 || v == 10000; }));
1588-
}
1603+
// --- VisitLessThan for value 5 ---
1604+
{
1605+
Literal lit_5(5);
1606+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_5));
1607+
CheckResult(result, collect_rows([](int32_t v) { return v < 5; }));
1608+
}
15891609

1590-
// --- VisitNotEqual for value 0 ---
1591-
{
1592-
Literal lit_0(0);
1593-
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_0));
1594-
CheckResult(result, collect_rows([](int32_t v) { return v != 0; }));
1610+
// --- VisitGreaterOrEqual for a high value near max ---
1611+
{
1612+
int32_t max_val =
1613+
static_cast<int32_t>(collect_rows([](int32_t) { return true; }).size()) / 3;
1614+
int32_t threshold = max_val - 2;
1615+
Literal lit_threshold(threshold);
1616+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterOrEqual(lit_threshold));
1617+
CheckResult(result, collect_rows([threshold](int32_t v) { return v >= threshold; }));
1618+
}
1619+
1620+
// --- VisitIn for scattered values ---
1621+
{
1622+
std::vector<Literal> in_literals = {Literal(0), Literal(500), Literal(10000)};
1623+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
1624+
CheckResult(result,
1625+
collect_rows([](int32_t v) { return v == 0 || v == 500 || v == 10000; }));
1626+
}
1627+
1628+
// --- VisitNotEqual for value 0 ---
1629+
{
1630+
Literal lit_0(0);
1631+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_0));
1632+
CheckResult(result, collect_rows([](int32_t v) { return v != 0; }));
1633+
}
15951634
}
15961635
}
15971636

@@ -1722,6 +1761,22 @@ TEST_P(BTreeGlobalIndexIntegrationTest, FinishWithEmptyData) {
17221761
ASSERT_NOK_WITH_MSG(writer->Finish(), "Should never write an empty btree index file");
17231762
}
17241763

1764+
TEST_P(BTreeGlobalIndexIntegrationTest, InvalidReadOptions) {
1765+
auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_, base_path_);
1766+
auto field = arrow::field("int_field", arrow::int32());
1767+
auto c_schema = CreateArrowSchema(field);
1768+
1769+
std::map<std::string, std::string> options = {{BtreeDefs::kBtreeIndexBlockSize, "4096"},
1770+
{BtreeDefs::kBtreeIndexCompression, GetParam()},
1771+
{BtreeDefs::kBtreeIndexReadBufferSize, "4GB"}};
1772+
ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
1773+
1774+
auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_, base_path_);
1775+
ASSERT_NOK_WITH_MSG(indexer->CreateReader(c_schema.get(), file_reader, /*files=*/{}, pool_),
1776+
"In BTreeGlobalIndexer::CreateReader: option btree-index.read-buffer-size "
1777+
"is 4GB, exceed INT_MAX or less than 0");
1778+
}
1779+
17251780
TEST_P(BTreeGlobalIndexIntegrationTest, TestIOException) {
17261781
bool run_complete = false;
17271782
auto io_hook = paimon::IOHook::GetInstance();

0 commit comments

Comments
 (0)