This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 6267e9c0 [rust] Reuse BTree data blocks across index lookups (#995)
6267e9c0 is described below
commit 6267e9c0a93987b6b8a7b37b0a750ce332e77857
Author: XiaoHongbo <[email protected]>
AuthorDate: Wed Sep 30 15:53:49 2026 +0800
[rust] Reuse BTree data blocks across index lookups (#995)
---
crates/paimon/src/btree/block.rs | 10 +
crates/paimon/src/btree/data_block_cache.rs | 104 +++++++++
crates/paimon/src/btree/mod.rs | 2 +
crates/paimon/src/btree/reader.rs | 38 +++-
crates/paimon/src/btree/tests.rs | 234 +++++++++++++++++++++
crates/paimon/src/spec/core_options.rs | 136 ++++++++++++
crates/paimon/src/table/global_index_scanner.rs | 8 +-
.../src/table/global_index_scanner/reader.rs | 21 +-
.../paimon/src/table/global_index_scanner/tests.rs | 6 +
.../sorted_global_index_build_builder/tests.rs | 3 +
crates/paimon/src/table/table_scan.rs | 1 +
11 files changed, 553 insertions(+), 10 deletions(-)
diff --git a/crates/paimon/src/btree/block.rs b/crates/paimon/src/btree/block.rs
index 6f5c513b..6bb8ec27 100644
--- a/crates/paimon/src/btree/block.rs
+++ b/crates/paimon/src/btree/block.rs
@@ -588,6 +588,16 @@ impl BlockReader {
}
}
+ pub(crate) fn retained_bytes(&self) -> usize {
+ let seek_bytes = match &self.seek_info {
+ SeekInfo::Aligned { .. } => 0,
+ SeekInfo::Unaligned { offsets } => offsets
+ .capacity()
+ .saturating_mul(std::mem::size_of::<i32>()),
+ };
+ self.data.capacity().saturating_add(seek_bytes)
+ }
+
#[allow(dead_code)]
pub fn record_count(&self) -> usize {
self.record_count
diff --git a/crates/paimon/src/btree/data_block_cache.rs
b/crates/paimon/src/btree/data_block_cache.rs
new file mode 100644
index 00000000..530dc001
--- /dev/null
+++ b/crates/paimon/src/btree/data_block_cache.rs
@@ -0,0 +1,104 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::btree::block::BlockReader;
+use lru::LruCache;
+use std::sync::{Arc, Mutex};
+
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub(super) struct DataBlockCacheKey {
+ pub(super) file: Arc<str>,
+ pub(super) offset: u64,
+ pub(super) size: u32,
+}
+
+struct Entry {
+ block: Arc<BlockReader>,
+ retained_bytes: usize,
+}
+
+struct State {
+ entries: LruCache<DataBlockCacheKey, Entry>,
+ retained_bytes: usize,
+}
+
+pub(crate) struct BTreeDataBlockCache {
+ max_bytes: usize,
+ state: Mutex<State>,
+}
+
+impl BTreeDataBlockCache {
+ pub(crate) fn new(max_bytes: usize) -> Self {
+ Self {
+ max_bytes,
+ state: Mutex::new(State {
+ entries: LruCache::unbounded(),
+ retained_bytes: 0,
+ }),
+ }
+ }
+
+ pub(super) fn get(&self, key: &DataBlockCacheKey) ->
Option<Arc<BlockReader>> {
+ if self.max_bytes == 0 {
+ return None;
+ }
+ self.state
+ .lock()
+ .unwrap_or_else(|error| error.into_inner())
+ .entries
+ .get(key)
+ .map(|entry| Arc::clone(&entry.block))
+ }
+
+ pub(super) fn put(&self, key: DataBlockCacheKey, block: Arc<BlockReader>)
-> Arc<BlockReader> {
+ if self.max_bytes == 0 {
+ return block;
+ }
+ let retained_bytes = block.retained_bytes();
+ if retained_bytes > self.max_bytes {
+ return block;
+ }
+
+ let mut state = self.state.lock().unwrap_or_else(|error|
error.into_inner());
+ if let Some(entry) = state.entries.get(&key) {
+ return Arc::clone(&entry.block);
+ }
+ state.retained_bytes =
state.retained_bytes.saturating_add(retained_bytes);
+ state.entries.put(
+ key,
+ Entry {
+ block: Arc::clone(&block),
+ retained_bytes,
+ },
+ );
+ while state.retained_bytes > self.max_bytes {
+ let Some((_, entry)) = state.entries.pop_lru() else {
+ break;
+ };
+ state.retained_bytes =
state.retained_bytes.saturating_sub(entry.retained_bytes);
+ }
+ block
+ }
+
+ #[cfg(test)]
+ pub(crate) fn retained_bytes(&self) -> usize {
+ self.state
+ .lock()
+ .unwrap_or_else(|error| error.into_inner())
+ .retained_bytes
+ }
+}
diff --git a/crates/paimon/src/btree/mod.rs b/crates/paimon/src/btree/mod.rs
index 5b7fc6fa..ddee2d62 100644
--- a/crates/paimon/src/btree/mod.rs
+++ b/crates/paimon/src/btree/mod.rs
@@ -38,6 +38,7 @@
mod block;
mod bloom_filter;
+mod data_block_cache;
mod footer;
pub(crate) mod key_serde;
mod meta;
@@ -52,6 +53,7 @@ pub use block::BlockCompressionType;
pub(crate) use block::{
compress_block, compress_codec_block, compute_crc32, decompress_block,
decompress_codec_block,
};
+pub(crate) use data_block_cache::BTreeDataBlockCache;
pub use footer::BTreeFileFooter;
pub use key_serde::{make_key_comparator, serialize_datum};
pub use meta::BTreeIndexMeta;
diff --git a/crates/paimon/src/btree/reader.rs
b/crates/paimon/src/btree/reader.rs
index 4e2f50e4..2e8f30e9 100644
--- a/crates/paimon/src/btree/reader.rs
+++ b/crates/paimon/src/btree/reader.rs
@@ -25,6 +25,7 @@
use crate::btree::block::{BlockHandle, BlockReader};
use crate::btree::bloom_filter::BloomFilter;
+use crate::btree::data_block_cache::{BTreeDataBlockCache, DataBlockCacheKey};
use crate::btree::footer::{BTreeFileFooter, BloomFilterHandle,
BTREE_FOOTER_ENCODED_LENGTH};
use crate::btree::key_serde::key_comparison_io_error;
use crate::btree::meta::BTreeIndexMeta;
@@ -35,6 +36,7 @@ use crate::spec::murmur_hash::hash_bytes;
use roaring::RoaringTreemap;
use std::cmp::Ordering;
use std::io;
+use std::sync::Arc;
use tokio::sync::OnceCell;
struct LazyBloomFilter {
@@ -72,6 +74,8 @@ pub struct BTreeIndexReader<F: Fn(&[u8], &[u8]) ->
crate::Result<Ordering>> {
key_comparator: F,
bloom_filter: Option<LazyBloomFilter>,
file_version: u32,
+ data_block_cache: Arc<BTreeDataBlockCache>,
+ data_block_cache_file: Arc<str>,
}
impl<F: Fn(&[u8], &[u8]) -> crate::Result<Ordering>> BTreeIndexReader<F> {
@@ -83,6 +87,25 @@ impl<F: Fn(&[u8], &[u8]) -> crate::Result<Ordering>>
BTreeIndexReader<F> {
file_size: u64,
meta: &BTreeIndexMeta,
key_comparator: F,
+ ) -> io::Result<Self> {
+ Self::open_with_data_block_cache(
+ reader,
+ file_size,
+ meta,
+ key_comparator,
+ Arc::new(BTreeDataBlockCache::new(0)),
+ Arc::from(""),
+ )
+ .await
+ }
+
+ pub(crate) async fn open_with_data_block_cache(
+ reader: Box<dyn FileRead>,
+ file_size: u64,
+ meta: &BTreeIndexMeta,
+ key_comparator: F,
+ data_block_cache: Arc<BTreeDataBlockCache>,
+ data_block_cache_file: Arc<str>,
) -> io::Result<Self> {
if file_size < BTREE_FOOTER_ENCODED_LENGTH as u64 {
return Err(io::Error::new(
@@ -128,6 +151,8 @@ impl<F: Fn(&[u8], &[u8]) -> crate::Result<Ordering>>
BTreeIndexReader<F> {
key_comparator,
bloom_filter,
file_version: footer.version,
+ data_block_cache,
+ data_block_cache_file,
})
}
@@ -287,14 +312,23 @@ impl<F: Fn(&[u8], &[u8]) -> crate::Result<Ordering>>
BTreeIndexReader<F> {
}
/// Read a data block from the file on demand.
- async fn read_data_block(&self, handle: &BlockHandle) ->
io::Result<BlockReader> {
+ async fn read_data_block(&self, handle: &BlockHandle) ->
io::Result<Arc<BlockReader>> {
+ let key = DataBlockCacheKey {
+ file: Arc::clone(&self.data_block_cache_file),
+ offset: handle.offset,
+ size: handle.size,
+ };
+ if let Some(block) = self.data_block_cache.get(&key) {
+ return Ok(block);
+ }
let end = handle.offset + handle.full_block_size() as u64;
let bytes = self
.reader
.read(handle.offset..end)
.await
.map_err(|e| io::Error::other(e.to_string()))?;
- read_block_from_bytes(&bytes, handle.size)
+ let block = Arc::new(read_block_from_bytes(&bytes, handle.size)?);
+ Ok(self.data_block_cache.put(key, block))
}
async fn bloom_might_contain(&self, key: &[u8]) -> io::Result<bool> {
diff --git a/crates/paimon/src/btree/tests.rs b/crates/paimon/src/btree/tests.rs
index 105e9b71..c9a0f20c 100644
--- a/crates/paimon/src/btree/tests.rs
+++ b/crates/paimon/src/btree/tests.rs
@@ -23,6 +23,7 @@ use crate::btree::query::IndexQuery;
use crate::btree::reader::BTreeIndexReader;
use crate::btree::test_util::{BytesFileRead, VecFileWrite};
use crate::btree::writer::BTreeIndexWriter;
+use crate::btree::BTreeDataBlockCache;
use crate::io::FileRead;
use crate::spec::murmur_hash::hash_bytes;
use crate::spec::{DataType, Datum, PredicateOperator, VarCharType};
@@ -823,6 +824,239 @@ async fn
test_sparse_in_query_reads_only_target_blocks_once() {
assert_eq!(ranges.lock().unwrap().len(), 3);
}
+#[tokio::test]
+async fn test_repeated_equal_queries_reuse_data_block() {
+ let buf = VecFileWrite::new();
+ let mut writer =
+ BTreeIndexWriter::new(Box::new(buf.clone()), 1_000_000,
BlockCompressionType::None);
+ for i in 0..100 {
+ writer.write(Some(&int_key(i)), i as i64).await.unwrap();
+ }
+
+ let write_result = writer.finish().await.unwrap();
+ let data = Bytes::from(buf.to_vec());
+ let file_size = data.len() as u64;
+
+ let uncached_ranges = Arc::new(Mutex::new(Vec::new()));
+ let uncached = BTreeIndexReader::open_with_data_block_cache(
+ Box::new(RecordingFileRead {
+ data: data.clone(),
+ ranges: Arc::clone(&uncached_ranges),
+ }),
+ file_size,
+ &write_result.meta,
+ int_cmp,
+ Arc::new(BTreeDataBlockCache::new(0)),
+ Arc::from("memory:/uncached.btree"),
+ )
+ .await
+ .unwrap();
+ uncached_ranges.lock().unwrap().clear();
+ for key in [1, 2, 3] {
+ uncached.query_equal(&int_key(key)).await.unwrap();
+ }
+ assert_eq!(uncached_ranges.lock().unwrap().len(), 3);
+
+ let ranges = Arc::new(Mutex::new(Vec::new()));
+ let reader = BTreeIndexReader::open_with_data_block_cache(
+ Box::new(RecordingFileRead {
+ data,
+ ranges: Arc::clone(&ranges),
+ }),
+ file_size,
+ &write_result.meta,
+ int_cmp,
+ Arc::new(BTreeDataBlockCache::new(1024 * 1024)),
+ Arc::from("memory:/first.btree"),
+ )
+ .await
+ .unwrap();
+ ranges.lock().unwrap().clear();
+
+ for key in [1, 2, 3] {
+ let rows = reader.query_equal(&int_key(key)).await.unwrap();
+ assert_eq!(rows.iter().collect::<Vec<_>>(), vec![key as u64]);
+ }
+ assert_eq!(ranges.lock().unwrap().len(), 1);
+}
+
+#[tokio::test]
+async fn test_data_block_cache_is_bounded_and_file_scoped() {
+ async fn write_file(row_id: i64) -> (Bytes,
crate::btree::writer::BTreeWriteResult) {
+ let buf = VecFileWrite::new();
+ let mut writer =
+ BTreeIndexWriter::new(Box::new(buf.clone()), 1_000_000,
BlockCompressionType::None);
+ writer.write(Some(&int_key(1)), row_id).await.unwrap();
+ let result = writer.finish().await.unwrap();
+ (Bytes::from(buf.to_vec()), result)
+ }
+
+ let cache = Arc::new(BTreeDataBlockCache::new(1024 * 1024));
+ let (first_data, first_result) = write_file(1).await;
+ let first_ranges = Arc::new(Mutex::new(Vec::new()));
+ let first = BTreeIndexReader::open_with_data_block_cache(
+ Box::new(RecordingFileRead {
+ data: first_data.clone(),
+ ranges: Arc::clone(&first_ranges),
+ }),
+ first_data.len() as u64,
+ &first_result.meta,
+ int_cmp,
+ Arc::clone(&cache),
+ Arc::from("memory:/first.btree"),
+ )
+ .await
+ .unwrap();
+
+ let (second_data, second_result) = write_file(2).await;
+ let second_ranges = Arc::new(Mutex::new(Vec::new()));
+ let second = BTreeIndexReader::open_with_data_block_cache(
+ Box::new(RecordingFileRead {
+ data: second_data.clone(),
+ ranges: Arc::clone(&second_ranges),
+ }),
+ second_data.len() as u64,
+ &second_result.meta,
+ int_cmp,
+ Arc::clone(&cache),
+ Arc::from("memory:/second.btree"),
+ )
+ .await
+ .unwrap();
+ first_ranges.lock().unwrap().clear();
+ second_ranges.lock().unwrap().clear();
+
+ assert_eq!(
+ first
+ .query_equal(&int_key(1))
+ .await
+ .unwrap()
+ .iter()
+ .collect::<Vec<_>>(),
+ vec![1]
+ );
+ assert_eq!(
+ second
+ .query_equal(&int_key(1))
+ .await
+ .unwrap()
+ .iter()
+ .collect::<Vec<_>>(),
+ vec![2]
+ );
+ assert_eq!(first_ranges.lock().unwrap().len(), 1);
+ assert_eq!(second_ranges.lock().unwrap().len(), 1);
+
+ let two_file_bytes = cache.retained_bytes();
+ assert!(two_file_bytes > 1);
+ let bounded_cache = Arc::new(BTreeDataBlockCache::new(two_file_bytes - 1));
+ let bounded_first_ranges = Arc::new(Mutex::new(Vec::new()));
+ let bounded_first = BTreeIndexReader::open_with_data_block_cache(
+ Box::new(RecordingFileRead {
+ data: first_data.clone(),
+ ranges: Arc::clone(&bounded_first_ranges),
+ }),
+ first_data.len() as u64,
+ &first_result.meta,
+ int_cmp,
+ Arc::clone(&bounded_cache),
+ Arc::from("memory:/bounded-first.btree"),
+ )
+ .await
+ .unwrap();
+ let bounded_second_ranges = Arc::new(Mutex::new(Vec::new()));
+ let bounded_second = BTreeIndexReader::open_with_data_block_cache(
+ Box::new(RecordingFileRead {
+ data: second_data.clone(),
+ ranges: Arc::clone(&bounded_second_ranges),
+ }),
+ second_data.len() as u64,
+ &second_result.meta,
+ int_cmp,
+ Arc::clone(&bounded_cache),
+ Arc::from("memory:/bounded-second.btree"),
+ )
+ .await
+ .unwrap();
+ bounded_first_ranges.lock().unwrap().clear();
+ bounded_second_ranges.lock().unwrap().clear();
+
+ bounded_first.query_equal(&int_key(1)).await.unwrap();
+ bounded_second.query_equal(&int_key(1)).await.unwrap();
+ bounded_first.query_equal(&int_key(1)).await.unwrap();
+ assert_eq!(bounded_first_ranges.lock().unwrap().len(), 2);
+ assert_eq!(bounded_second_ranges.lock().unwrap().len(), 1);
+ assert!(bounded_cache.retained_bytes() < two_file_bytes);
+
+ let ranges = Arc::new(Mutex::new(Vec::new()));
+ let uncached = BTreeIndexReader::open_with_data_block_cache(
+ Box::new(RecordingFileRead {
+ data: first_data.clone(),
+ ranges: Arc::clone(&ranges),
+ }),
+ first_data.len() as u64,
+ &first_result.meta,
+ int_cmp,
+ Arc::new(BTreeDataBlockCache::new(0)),
+ Arc::from("memory:/uncached.btree"),
+ )
+ .await
+ .unwrap();
+ ranges.lock().unwrap().clear();
+ uncached.query_equal(&int_key(1)).await.unwrap();
+ uncached.query_equal(&int_key(1)).await.unwrap();
+ assert_eq!(ranges.lock().unwrap().len(), 2);
+}
+
+#[tokio::test]
+async fn test_data_block_cache_evicts_by_decoded_bytes() {
+ let buf = VecFileWrite::new();
+ let mut writer = BTreeIndexWriter::new(Box::new(buf.clone()), 64,
BlockCompressionType::None);
+ for i in 0..100 {
+ writer.write(Some(&int_key(i)), i as i64).await.unwrap();
+ }
+ let write_result = writer.finish().await.unwrap();
+ let data = Bytes::from(buf.to_vec());
+ let file_size = data.len() as u64;
+
+ let measuring_cache = Arc::new(BTreeDataBlockCache::new(usize::MAX));
+ let measuring_reader = BTreeIndexReader::open_with_data_block_cache(
+ Box::new(BytesFileRead(data.clone())),
+ file_size,
+ &write_result.meta,
+ int_cmp,
+ Arc::clone(&measuring_cache),
+ Arc::from("memory:/measuring.btree"),
+ )
+ .await
+ .unwrap();
+ measuring_reader.query_equal(&int_key(0)).await.unwrap();
+ measuring_reader.query_equal(&int_key(99)).await.unwrap();
+ let two_block_bytes = measuring_cache.retained_bytes();
+ assert!(two_block_bytes > 2);
+
+ let ranges = Arc::new(Mutex::new(Vec::new()));
+ let reader = BTreeIndexReader::open_with_data_block_cache(
+ Box::new(RecordingFileRead {
+ data,
+ ranges: Arc::clone(&ranges),
+ }),
+ file_size,
+ &write_result.meta,
+ int_cmp,
+ Arc::new(BTreeDataBlockCache::new(two_block_bytes - 1)),
+ Arc::from("memory:/bounded.btree"),
+ )
+ .await
+ .unwrap();
+ ranges.lock().unwrap().clear();
+
+ reader.query_equal(&int_key(0)).await.unwrap();
+ reader.query_equal(&int_key(99)).await.unwrap();
+ reader.query_equal(&int_key(0)).await.unwrap();
+ assert_eq!(ranges.lock().unwrap().len(), 3);
+}
+
#[tokio::test]
async fn test_sparse_in_query_seeks_each_key_within_target_block() {
let buf = VecFileWrite::new();
diff --git a/crates/paimon/src/spec/core_options.rs
b/crates/paimon/src/spec/core_options.rs
index b4730a7c..d95c9767 100644
--- a/crates/paimon/src/spec/core_options.rs
+++ b/crates/paimon/src/spec/core_options.rs
@@ -37,6 +37,8 @@ const GLOBAL_INDEX_COLUMN_UPDATE_ACTION_OPTION: &str =
"global-index.column-upda
pub(crate) const INDEX_FILE_IN_DATA_FILE_DIR_OPTION: &str =
"index-file-in-data-file-dir";
const SORTED_INDEX_RECORDS_PER_RANGE_OPTION: &str =
"sorted-index.records-per-range";
const BTREE_INDEX_RECORDS_PER_RANGE_OPTION: &str =
"btree-index.records-per-range";
+const BTREE_INDEX_CACHE_SIZE_OPTION: &str = "btree-index.cache-size";
+const BTREE_INDEX_HIGH_PRIORITY_POOL_RATIO_OPTION: &str =
"btree-index.high-priority-pool-ratio";
const BTREE_INDEX_FALLBACK_SCAN_MAX_SIZE_OPTION: &str =
"btree-index.fallback-scan-max-size";
const BITMAP_INDEX_FALLBACK_SCAN_MAX_SIZE_OPTION: &str =
"bitmap-index.fallback-scan-max-size";
const SOURCE_SPLIT_TARGET_SIZE_OPTION: &str = "source.split.target-size";
@@ -169,6 +171,8 @@ const MAX_GLOBAL_INDEX_THREAD_NUM: i64 = {
}
};
const DEFAULT_GLOBAL_INDEX_FALLBACK_SCAN_MAX_SIZE: i64 = 256 * 1024 * 1024;
+const DEFAULT_BTREE_INDEX_CACHE_SIZE: i64 = 128 * 1024 * 1024;
+const DEFAULT_BTREE_INDEX_HIGH_PRIORITY_POOL_RATIO: f64 = 0.1;
const BLOB_AS_DESCRIPTOR_OPTION: &str = "blob-as-descriptor";
pub(crate) const BLOB_FIELD_OPTION: &str = "blob-field";
pub(crate) const BLOB_DESCRIPTOR_FIELD_OPTION: &str = "blob-descriptor-field";
@@ -1021,6 +1025,70 @@ impl<'a> CoreOptions<'a> {
self.fallback_scan_max_size(BTREE_INDEX_FALLBACK_SCAN_MAX_SIZE_OPTION)
}
+ pub fn btree_index_cache_size(&self) -> crate::Result<usize> {
+ let value = match self.options.get(BTREE_INDEX_CACHE_SIZE_OPTION) {
+ Some(raw) => parse_memory_size(raw).ok_or_else(||
crate::Error::DataInvalid {
+ message: format!(
+ "Option '{}' must be a valid memory size, got: {}",
+ BTREE_INDEX_CACHE_SIZE_OPTION, raw
+ ),
+ source: None,
+ })?,
+ None => DEFAULT_BTREE_INDEX_CACHE_SIZE,
+ };
+ if value < 0 {
+ return Err(crate::Error::DataInvalid {
+ message: format!(
+ "Option '{}' must be greater than or equal to 0, got: {}",
+ BTREE_INDEX_CACHE_SIZE_OPTION, value
+ ),
+ source: None,
+ });
+ }
+ usize::try_from(value).map_err(|_| crate::Error::DataInvalid {
+ message: format!(
+ "Option '{}' is too large: {}",
+ BTREE_INDEX_CACHE_SIZE_OPTION, value
+ ),
+ source: None,
+ })
+ }
+
+ pub fn btree_index_high_priority_pool_ratio(&self) -> crate::Result<f64> {
+ let value = match self
+ .options
+ .get(BTREE_INDEX_HIGH_PRIORITY_POOL_RATIO_OPTION)
+ {
+ Some(raw) => raw
+ .trim()
+ .parse::<f64>()
+ .map_err(|_| crate::Error::DataInvalid {
+ message: format!(
+ "Option '{}' must be a valid number, got: {}",
+ BTREE_INDEX_HIGH_PRIORITY_POOL_RATIO_OPTION, raw
+ ),
+ source: None,
+ })?,
+ None => DEFAULT_BTREE_INDEX_HIGH_PRIORITY_POOL_RATIO,
+ };
+ if !(0.0..1.0).contains(&value) {
+ return Err(crate::Error::DataInvalid {
+ message: format!(
+ "Option '{}' must be in the range [0, 1), got: {}",
+ BTREE_INDEX_HIGH_PRIORITY_POOL_RATIO_OPTION, value
+ ),
+ source: None,
+ });
+ }
+ Ok(value)
+ }
+
+ pub fn btree_index_data_block_cache_size(&self) -> crate::Result<usize> {
+ let total = self.btree_index_cache_size()?;
+ let high_priority_ratio = self.btree_index_high_priority_pool_ratio()?;
+ Ok((total as f64 * (1.0 - high_priority_ratio)) as usize)
+ }
+
pub fn bitmap_index_fallback_scan_max_size(&self) -> crate::Result<i64> {
self.fallback_scan_max_size(BITMAP_INDEX_FALLBACK_SCAN_MAX_SIZE_OPTION)
}
@@ -2463,6 +2531,74 @@ mod tests {
}
}
+ #[test]
+ fn test_btree_index_cache_size_matches_java_default_and_validates_values()
{
+ assert_eq!(
+ CoreOptions::new(&HashMap::new())
+ .btree_index_cache_size()
+ .unwrap(),
+ 128 * 1024 * 1024
+ );
+
+ let disabled =
+ HashMap::from([(BTREE_INDEX_CACHE_SIZE_OPTION.to_string(),
"0".to_string())]);
+ assert_eq!(
+ CoreOptions::new(&disabled)
+ .btree_index_cache_size()
+ .unwrap(),
+ 0
+ );
+
+ for value in ["-1", "invalid"] {
+ let options =
+ HashMap::from([(BTREE_INDEX_CACHE_SIZE_OPTION.to_string(),
value.to_string())]);
+ let error = CoreOptions::new(&options)
+ .btree_index_cache_size()
+ .expect_err("invalid BTree cache size should fail");
+ assert!(error.to_string().contains(BTREE_INDEX_CACHE_SIZE_OPTION));
+ }
+ }
+
+ #[test]
+ fn test_btree_index_data_block_cache_size_matches_java_pool_split() {
+ let empty = HashMap::new();
+ let defaults = CoreOptions::new(&empty);
+ assert_eq!(
+ defaults.btree_index_data_block_cache_size().unwrap(),
+ (128.0 * 1024.0 * 1024.0 * 0.9) as usize
+ );
+
+ let options = HashMap::from([
+ (
+ BTREE_INDEX_CACHE_SIZE_OPTION.to_string(),
+ (128 * 1024 * 1024).to_string(),
+ ),
+ (
+ BTREE_INDEX_HIGH_PRIORITY_POOL_RATIO_OPTION.to_string(),
+ "0.9".to_string(),
+ ),
+ ]);
+ assert_eq!(
+ CoreOptions::new(&options)
+ .btree_index_data_block_cache_size()
+ .unwrap(),
+ (128.0 * 1024.0 * 1024.0 * 0.1) as usize
+ );
+
+ for value in ["-0.1", "1", "NaN", "invalid"] {
+ let options = HashMap::from([(
+ BTREE_INDEX_HIGH_PRIORITY_POOL_RATIO_OPTION.to_string(),
+ value.to_string(),
+ )]);
+ let error = CoreOptions::new(&options)
+ .btree_index_data_block_cache_size()
+ .expect_err("invalid BTree cache ratio should fail");
+ assert!(error
+ .to_string()
+ .contains(BTREE_INDEX_HIGH_PRIORITY_POOL_RATIO_OPTION));
+ }
+ }
+
#[test]
fn test_file_formats_are_normalized_to_lowercase() {
// Java routes every file-format option through
diff --git a/crates/paimon/src/table/global_index_scanner.rs
b/crates/paimon/src/table/global_index_scanner.rs
index 8c721b8c..fdd21a4e 100644
--- a/crates/paimon/src/table/global_index_scanner.rs
+++ b/crates/paimon/src/table/global_index_scanner.rs
@@ -40,7 +40,7 @@ use super::global_index_types::{
normalize_queryable_global_index_type, BITMAP_GLOBAL_INDEX_TYPE,
BTREE_GLOBAL_INDEX_TYPE,
FM_GLOBAL_INDEX_TYPE, MULTIVALUE_GLOBAL_INDEX_TYPE,
};
-use crate::btree::{BTreeIndexMeta, BTreeIndexReader};
+use crate::btree::{BTreeDataBlockCache, BTreeIndexMeta, BTreeIndexReader};
use crate::fm_index::{manifest_row_range, FMReadContext, FMReadOptions};
use crate::io::FileIO;
use crate::spec::{DataField, FileKind, GlobalIndexSearchMode,
IndexManifestEntry, Predicate};
@@ -107,6 +107,7 @@ pub(crate) struct GlobalIndexScanner {
/// Scan-scoped shard I/O budget shared by all indexed fields.
query_semaphore: Arc<Semaphore>,
btree_fallback_scan_max_size: i64,
+ btree_data_block_cache: Arc<BTreeDataBlockCache>,
bitmap_fallback_scan_max_size: i64,
fm_read_options: FMReadOptions,
fm_read_context: Arc<FMReadContext>,
@@ -141,6 +142,7 @@ impl GlobalIndexScanner {
table_path,
global_index_thread_num,
btree_fallback_scan_max_size,
+ 128 * 1024 * 1024,
bitmap_fallback_scan_max_size,
index_entries,
schema_fields,
@@ -154,6 +156,7 @@ impl GlobalIndexScanner {
table_path: &str,
global_index_thread_num: usize,
btree_fallback_scan_max_size: i64,
+ btree_data_block_cache_size: usize,
bitmap_fallback_scan_max_size: i64,
index_entries: &[IndexManifestEntry],
schema_fields: &[DataField],
@@ -307,6 +310,7 @@ impl GlobalIndexScanner {
global_index_thread_num,
query_semaphore: Arc::new(Semaphore::new(global_index_thread_num)),
btree_fallback_scan_max_size,
+ btree_data_block_cache:
Arc::new(BTreeDataBlockCache::new(btree_data_block_cache_size)),
bitmap_fallback_scan_max_size,
fm_read_options,
fm_read_context:
Arc::new(FMReadContext::new(fm_read_options.cache_size)),
@@ -334,6 +338,7 @@ pub(crate) struct GlobalIndexEvaluation<'a> {
pub(crate) search_mode: GlobalIndexSearchMode,
pub(crate) global_index_thread_num: usize,
pub(crate) btree_fallback_scan_max_size: i64,
+ pub(crate) btree_data_block_cache_size: usize,
pub(crate) bitmap_fallback_scan_max_size: i64,
pub(crate) fm_read_options: FMReadOptions,
pub(crate) next_row_id: Option<i64>,
@@ -348,6 +353,7 @@ pub(crate) async fn evaluate_global_index(
evaluation.table_path,
evaluation.global_index_thread_num,
evaluation.btree_fallback_scan_max_size,
+ evaluation.btree_data_block_cache_size,
evaluation.bitmap_fallback_scan_max_size,
evaluation.index_entries,
evaluation.schema_fields,
diff --git a/crates/paimon/src/table/global_index_scanner/reader.rs
b/crates/paimon/src/table/global_index_scanner/reader.rs
index fc011e10..e877176f 100644
--- a/crates/paimon/src/table/global_index_scanner/reader.rs
+++ b/crates/paimon/src/table/global_index_scanner/reader.rs
@@ -245,13 +245,20 @@ impl GlobalIndexScanner {
let file_reader = input.reader().await?;
let cmp = make_key_comparator(data_type);
- BTreeIndexReader::open(Box::new(file_reader), file_size, meta, cmp)
- .await
- .map(OpenedGlobalIndexReader::BTree)
- .map_err(|e| crate::Error::DataInvalid {
- message: format!("Failed to open BTree index file:
{resolved_path}"),
- source: Some(Box::new(e)),
- })
+ BTreeIndexReader::open_with_data_block_cache(
+ Box::new(file_reader),
+ file_size,
+ meta,
+ cmp,
+ Arc::clone(&self.btree_data_block_cache),
+ Arc::from(resolved_path.as_str()),
+ )
+ .await
+ .map(OpenedGlobalIndexReader::BTree)
+ .map_err(|e| crate::Error::DataInvalid {
+ message: format!("Failed to open BTree index file:
{resolved_path}"),
+ source: Some(Box::new(e)),
+ })
}
async fn open_reader_for_entry(
diff --git a/crates/paimon/src/table/global_index_scanner/tests.rs
b/crates/paimon/src/table/global_index_scanner/tests.rs
index fc055c95..c28a59b3 100644
--- a/crates/paimon/src/table/global_index_scanner/tests.rs
+++ b/crates/paimon/src/table/global_index_scanner/tests.rs
@@ -693,6 +693,7 @@ async fn evaluate_global_index_fast_with_fallback_size(
search_mode: GlobalIndexSearchMode::Fast,
global_index_thread_num: 32,
btree_fallback_scan_max_size,
+ btree_data_block_cache_size: 128 * 1024 * 1024,
bitmap_fallback_scan_max_size,
fm_read_options: FMReadOptions::default(),
next_row_id: None,
@@ -961,6 +962,7 @@ async fn
test_btree_all_match_coverage_nulls_and_boolean_siblings() {
search_mode: mode,
global_index_thread_num: 2,
btree_fallback_scan_max_size: 0,
+ btree_data_block_cache_size: 128 * 1024 * 1024,
bitmap_fallback_scan_max_size: 0,
fm_read_options: FMReadOptions::default(),
next_row_id: Some(220),
@@ -1199,6 +1201,7 @@ async fn
test_scalar_optimization_empty_range_never_opens_index() {
search_mode: mode,
global_index_thread_num: 2,
btree_fallback_scan_max_size: 0,
+ btree_data_block_cache_size: 128 * 1024 * 1024,
bitmap_fallback_scan_max_size: 0,
fm_read_options: FMReadOptions::default(),
next_row_id: Some(220),
@@ -2420,6 +2423,7 @@ async fn
test_evaluate_global_index_full_mode_includes_unindexed_tail() {
search_mode: GlobalIndexSearchMode::Full,
global_index_thread_num: 32,
btree_fallback_scan_max_size: i64::MAX,
+ btree_data_block_cache_size: 128 * 1024 * 1024,
bitmap_fallback_scan_max_size: i64::MAX,
fm_read_options: FMReadOptions::default(),
next_row_id: Some(150),
@@ -2474,6 +2478,7 @@ async fn
test_evaluate_global_index_and_uses_evaluated_field_coverage_for_raw_fa
search_mode: GlobalIndexSearchMode::Full,
global_index_thread_num: 32,
btree_fallback_scan_max_size: i64::MAX,
+ btree_data_block_cache_size: 128 * 1024 * 1024,
bitmap_fallback_scan_max_size: i64::MAX,
fm_read_options: FMReadOptions::default(),
next_row_id: Some(100),
@@ -2509,6 +2514,7 @@ async fn
test_evaluate_global_index_detail_mode_uses_data_ranges() {
search_mode: GlobalIndexSearchMode::Detail,
global_index_thread_num: 32,
btree_fallback_scan_max_size: i64::MAX,
+ btree_data_block_cache_size: 128 * 1024 * 1024,
bitmap_fallback_scan_max_size: i64::MAX,
fm_read_options: FMReadOptions::default(),
next_row_id: Some(150),
diff --git a/crates/paimon/src/table/sorted_global_index_build_builder/tests.rs
b/crates/paimon/src/table/sorted_global_index_build_builder/tests.rs
index aa0d0551..b19d81ff 100644
--- a/crates/paimon/src/table/sorted_global_index_build_builder/tests.rs
+++ b/crates/paimon/src/table/sorted_global_index_build_builder/tests.rs
@@ -793,6 +793,7 @@ async fn assert_btree_build_version(version: u32) {
search_mode: GlobalIndexSearchMode::Fast,
global_index_thread_num: 32,
btree_fallback_scan_max_size: i64::MAX,
+ btree_data_block_cache_size: 128 * 1024 * 1024,
bitmap_fallback_scan_max_size: i64::MAX,
fm_read_options: crate::fm_index::FMReadOptions::default(),
next_row_id: snapshot.next_row_id(),
@@ -1576,6 +1577,7 @@ async fn
test_execute_writes_bitmap_index_manifest_and_java_file() {
search_mode: GlobalIndexSearchMode::Fast,
global_index_thread_num: 32,
btree_fallback_scan_max_size: i64::MAX,
+ btree_data_block_cache_size: 128 * 1024 * 1024,
bitmap_fallback_scan_max_size: i64::MAX,
fm_read_options: crate::fm_index::FMReadOptions::default(),
next_row_id: snapshot.next_row_id(),
@@ -1689,6 +1691,7 @@ async fn
test_execute_multivalue_index_and_array_queries_end_to_end() {
search_mode: GlobalIndexSearchMode::Fast,
global_index_thread_num: 32,
btree_fallback_scan_max_size: i64::MAX,
+ btree_data_block_cache_size: 128 * 1024 * 1024,
bitmap_fallback_scan_max_size: i64::MAX,
fm_read_options: crate::fm_index::FMReadOptions::default(),
next_row_id: snapshot.next_row_id(),
diff --git a/crates/paimon/src/table/table_scan.rs
b/crates/paimon/src/table/table_scan.rs
index e2a26032..ceb4bc85 100644
--- a/crates/paimon/src/table/table_scan.rs
+++ b/crates/paimon/src/table/table_scan.rs
@@ -1765,6 +1765,7 @@ impl<'a> PaimonTableScan<'a> {
search_mode: settings.search_mode,
global_index_thread_num: settings.thread_num,
btree_fallback_scan_max_size:
core_options.btree_index_fallback_scan_max_size()?,
+ btree_data_block_cache_size:
core_options.btree_index_data_block_cache_size()?,
bitmap_fallback_scan_max_size: core_options
.bitmap_index_fallback_scan_max_size()?,
fm_read_options: if index_entries