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 c120ac3b [core] Reduce global index planning IO (#699)
c120ac3b is described below
commit c120ac3b03c077c684bd9b194b1f4a92e2d6b898
Author: XiaoHongbo <[email protected]>
AuthorDate: Mon Aug 10 10:39:40 2026 +0800
[core] Reduce global index planning IO (#699)
---
crates/paimon/src/table/global_index_scanner.rs | 106 +++++++++++++++++-------
crates/paimon/src/table/table_scan.rs | 98 +++++++++++++++++++---
2 files changed, 163 insertions(+), 41 deletions(-)
diff --git a/crates/paimon/src/table/global_index_scanner.rs
b/crates/paimon/src/table/global_index_scanner.rs
index 80cf6c68..9af69b59 100644
--- a/crates/paimon/src/table/global_index_scanner.rs
+++ b/crates/paimon/src/table/global_index_scanner.rs
@@ -826,22 +826,26 @@ impl GlobalIndexScanner {
/// Get a cached reader or open a new one for the given file.
async fn get_or_open_reader(
&self,
- file_name: &str,
+ entry: &GlobalIndexEntry,
meta: &BTreeIndexMeta,
data_type: &DataType,
) -> Result<OpenedGlobalIndexReader> {
// Try to take from cache
{
let mut cache = self.reader_cache.lock().unwrap();
- if let Some(reader) = cache.remove(file_name) {
+ if let Some(reader) = cache.remove(&entry.file_name) {
return Ok(OpenedGlobalIndexReader::BTree(reader));
}
}
// Open new reader
- let path = format!("{}/{INDEX_DIR}/{}", self.table_path, file_name);
+ let path = format!("{}/{INDEX_DIR}/{}", self.table_path,
entry.file_name);
let input = self.file_io.new_input(&path)?;
- let file_size = input.metadata().await?.size;
+ let file_size = if entry.file_size > 0 {
+ entry.file_size as u64
+ } else {
+ input.metadata().await?.size
+ };
let file_reader = input.reader().await?;
let cmp = make_key_comparator(data_type);
@@ -849,7 +853,7 @@ impl GlobalIndexScanner {
.await
.map(OpenedGlobalIndexReader::BTree)
.map_err(|e| crate::Error::DataInvalid {
- message: format!("Failed to open BTree index file:
{file_name}"),
+ message: format!("Failed to open BTree index file: {}",
entry.file_name),
source: Some(Box::new(e)),
})
}
@@ -861,12 +865,9 @@ impl GlobalIndexScanner {
data_type: &DataType,
) -> Result<OpenedGlobalIndexReader> {
match entry.index_type {
- GlobalIndexFileKind::BTree => {
- self.get_or_open_reader(&entry.file_name, meta, data_type)
- .await
- }
+ GlobalIndexFileKind::BTree => self.get_or_open_reader(entry, meta,
data_type).await,
GlobalIndexFileKind::Bitmap => self
- .open_bitmap_reader(&entry.file_name)
+ .open_bitmap_reader(entry)
.await
.map(OpenedGlobalIndexReader::Bitmap)
.map_err(|e| crate::Error::DataInvalid {
@@ -881,18 +882,22 @@ impl GlobalIndexScanner {
async fn open_bitmap_reader(
&self,
- file_name: &str,
+ entry: &GlobalIndexEntry,
) -> std::io::Result<BitmapGlobalIndexReader> {
- let path = format!("{}/{INDEX_DIR}/{}", self.table_path, file_name);
+ let path = format!("{}/{INDEX_DIR}/{}", self.table_path,
entry.file_name);
let input = self
.file_io
.new_input(&path)
.map_err(|e| std::io::Error::other(e.to_string()))?;
- let file_size = input
- .metadata()
- .await
- .map_err(|e| std::io::Error::other(e.to_string()))?
- .size;
+ let file_size = if entry.file_size > 0 {
+ entry.file_size as u64
+ } else {
+ input
+ .metadata()
+ .await
+ .map_err(|e| std::io::Error::other(e.to_string()))?
+ .size
+ };
let file_reader = input
.reader()
.await
@@ -2168,10 +2173,14 @@ mod tests {
#[tokio::test]
async fn test_evaluate_global_index_eq() {
- let (file_io, table_path, file_name, _tmp) =
+ let (file_io, table_path, file_name, tmp) =
setup_testdata_table("btree_int_100_no_compress.bin");
let meta = BTreeIndexMeta::new(Some(le_int_key(0)),
Some(le_int_key(198)), false);
- let entries = vec![make_global_index_entry(&file_name, 1, 0, 99,
&meta)];
+ let mut entry = make_global_index_entry(&file_name, 1, 0, 99, &meta);
+ entry.index_file.file_size =
std::fs::metadata(tmp.path().join("index").join(&file_name))
+ .unwrap()
+ .len() as i64;
+ let entries = vec![entry];
let fields = int_schema_fields();
// key=50 -> row_id=25, offset by row_range_start=0 -> global row_id=25
@@ -2191,6 +2200,31 @@ mod tests {
assert_eq!(ranges, vec![RowRange::new(25, 25)]);
}
+ #[tokio::test]
+ async fn test_evaluate_global_index_uses_known_file_size() {
+ let (file_io, table_path, file_name, _tmp) =
+ setup_testdata_table("btree_int_100_no_compress.bin");
+ let meta = BTreeIndexMeta::new(Some(le_int_key(0)),
Some(le_int_key(198)), false);
+ let mut entry = make_global_index_entry(&file_name, 1, 0, 99, &meta);
+ entry.index_file.file_size = 1;
+
+ let error = evaluate_global_index_fast(
+ &file_io,
+ &table_path,
+ &[entry],
+ &[int_eq("id", 0, 50)],
+ &int_schema_fields(),
+ )
+ .await
+ .expect_err("the known file size should be used without a metadata
lookup");
+
+ assert!(matches!(
+ error,
+ crate::Error::DataInvalid { message, .. }
+ if message.contains("Failed to open BTree index file")
+ ));
+ }
+
#[tokio::test]
async fn test_missing_index_meta_returns_error() {
let (file_io, table_path, file_name, tmp) =
@@ -2304,15 +2338,19 @@ mod tests {
#[tokio::test]
async fn test_evaluate_java_bitmap_golden_index_eq_and_null() {
let data_type =
DataType::VarChar(crate::spec::VarCharType::string_type());
- let (file_io, table_path, file_name, meta, _tmp) =
setup_java_bitmap_testdata_table();
- let entries = vec![make_global_index_entry_with_type(
+ let (file_io, table_path, file_name, meta, tmp) =
setup_java_bitmap_testdata_table();
+ let mut entry = make_global_index_entry_with_type(
BITMAP_GLOBAL_INDEX_TYPE,
&file_name,
1,
100,
109,
&meta,
- )];
+ );
+ entry.index_file.file_size =
std::fs::metadata(tmp.path().join("index").join(&file_name))
+ .unwrap()
+ .len() as i64;
+ let entries = vec![entry];
let fields = string_schema_fields();
assert_eq!(meta.first_key, Some(b"alpha".to_vec()));
assert_eq!(meta.last_key, Some(b"office".to_vec()));
@@ -2659,15 +2697,20 @@ mod tests {
#[tokio::test]
async fn test_evaluate_java_bitmap_golden_index_string_fallback_scan() {
let data_type =
DataType::VarChar(crate::spec::VarCharType::string_type());
- let (file_io, table_path, file_name, meta, _tmp) =
setup_java_bitmap_testdata_table();
- let entries = vec![make_global_index_entry_with_type(
+ let (file_io, table_path, file_name, meta, tmp) =
setup_java_bitmap_testdata_table();
+ let file_size =
std::fs::metadata(tmp.path().join("index").join(&file_name))
+ .unwrap()
+ .len() as i64;
+ let mut entry = make_global_index_entry_with_type(
BITMAP_GLOBAL_INDEX_TYPE,
&file_name,
1,
100,
109,
&meta,
- )];
+ );
+ entry.index_file.file_size = file_size;
+ let entries = vec![entry];
let fields = string_schema_fields();
let ends_with_predicates = vec![Predicate::Leaf {
@@ -2746,15 +2789,16 @@ mod tests {
.unwrap();
assert_eq!(less_than_result.unwrap(), vec![RowRange::new(100, 102)]);
- let mut over_limit_entries = vec![make_global_index_entry_with_type(
+ let mut over_limit_entry = make_global_index_entry_with_type(
BITMAP_GLOBAL_INDEX_TYPE,
&file_name,
1,
100,
109,
&meta,
- )];
- over_limit_entries[0].index_file.file_size = 2;
+ );
+ over_limit_entry.index_file.file_size = file_size;
+ let over_limit_entries = vec![over_limit_entry];
let over_limit_less_than =
evaluate_global_index_fast_with_fallback_size(
&file_io,
&table_path,
@@ -2762,7 +2806,7 @@ mod tests {
&less_than_predicates,
&fields,
i64::MAX,
- 1,
+ file_size - 1,
)
.await
.unwrap();
@@ -2785,7 +2829,7 @@ mod tests {
&no_match_contains,
&fields,
i64::MAX,
- 1,
+ file_size - 1,
)
.await
.unwrap();
@@ -2817,7 +2861,7 @@ mod tests {
&direct_with_over_limit_fallback,
&fields,
i64::MAX,
- 1,
+ file_size - 1,
)
.await
.unwrap();
diff --git a/crates/paimon/src/table/table_scan.rs
b/crates/paimon/src/table/table_scan.rs
index 471606a1..7712eb82 100644
--- a/crates/paimon/src/table/table_scan.rs
+++ b/crates/paimon/src/table/table_scan.rs
@@ -400,6 +400,25 @@ fn retain_index_manifest_entry(
&&
normalize_sorted_global_index_type(&entry.index_file.index_type).is_some())
}
+fn retain_index_manifest_entry_for_scan(
+ entry: &IndexManifestEntry,
+ global_index_needed: bool,
+ deletion_vectors_needed: bool,
+ partition_filter: Option<&PartitionFilter>,
+) -> crate::Result<bool> {
+ if !retain_index_manifest_entry(entry, global_index_needed,
deletion_vectors_needed) {
+ return Ok(false);
+ }
+ // Deletion vectors are selected by partition and bucket when splits are
built.
+ if
normalize_sorted_global_index_type(&entry.index_file.index_type).is_none() {
+ return Ok(true);
+ }
+ partition_filter
+ .map(|filter| filter.matches_entry(&entry.partition))
+ .transpose()
+ .map(|matched| matched.unwrap_or(true))
+}
+
/// Builds a map from (partition, bucket) to (data_file_name -> DeletionFile)
from index manifest entries.
/// Only considers ADD entries with index_type "DELETION_VECTORS" and their
deletion_vectors_ranges.
fn build_deletion_files_map(
@@ -1229,13 +1248,17 @@ impl<'a> PaimonTableScan<'a> {
};
let table_path = self.table.location().trim_end_matches('/');
let path =
format!("{table_path}/{MANIFEST_DIR}/{index_manifest_name}");
- let entries = IndexManifest::read(self.table.file_io(), &path)
- .await?
- .into_iter()
- .filter(|entry| {
- retain_index_manifest_entry(entry, global_index_needed,
deletion_vectors_needed)
- })
- .collect();
+ let mut entries = Vec::new();
+ for entry in IndexManifest::read(self.table.file_io(), &path).await? {
+ if retain_index_manifest_entry_for_scan(
+ &entry,
+ global_index_needed,
+ deletion_vectors_needed,
+ self.partition_filter.as_ref(),
+ )? {
+ entries.push(entry);
+ }
+ }
Ok(Some(entries))
}
@@ -2034,9 +2057,9 @@ mod tests {
data_evolution_row_range_groups, data_file_overlaps_row_range_index,
group_data_files_by_partition_bucket,
manifest_file_overlaps_row_range_index,
prune_data_evolution_group_by_read_fields, retain_index_manifest_entry,
- retain_manifest_entry_row_ranges, retain_manifest_row_ranges,
- should_skip_level_zero_for_scan, split_row_ranges_for_files,
LimitPushdownAccumulator,
- PaimonTableScan, RowRangeIndex, TableScan,
+ retain_index_manifest_entry_for_scan, retain_manifest_entry_row_ranges,
+ retain_manifest_row_ranges, should_skip_level_zero_for_scan,
split_row_ranges_for_files,
+ LimitPushdownAccumulator, PaimonTableScan, RowRangeIndex, TableScan,
};
use crate::catalog::Identifier;
use crate::io::FileIOBuilder;
@@ -3998,6 +4021,61 @@ mod tests {
);
}
+ #[test]
+ fn test_retain_index_manifest_entries_for_selected_partitions() {
+ let partition = |value| {
+ let mut builder = BinaryRowBuilder::new(1);
+ builder.write_int(0, value);
+ builder.build_serialized()
+ };
+ let matching_partition = partition(7);
+ let filter = PartitionFilter::from_partition_set(
+ HashSet::from([matching_partition.clone()]),
+ &[DataField::new(
+ 0,
+ "dt".to_string(),
+ DataType::Int(IntType::new()),
+ )],
+ )
+ .unwrap();
+ let entry = |partition, index_type: &str| IndexManifestEntry {
+ version: 1,
+ kind: FileKind::Add,
+ partition,
+ bucket: 0,
+ index_file: IndexFileMeta {
+ index_type: index_type.to_string(),
+ file_name: "btree.idx".to_string(),
+ file_size: 1,
+ row_count: 1,
+ deletion_vectors_ranges: None,
+ global_index_meta: None,
+ },
+ };
+
+ assert!(retain_index_manifest_entry_for_scan(
+ &entry(matching_partition, "btree"),
+ true,
+ false,
+ Some(&filter),
+ )
+ .unwrap());
+ assert!(!retain_index_manifest_entry_for_scan(
+ &entry(partition(8), "btree"),
+ true,
+ false,
+ Some(&filter),
+ )
+ .unwrap());
+ assert!(retain_index_manifest_entry_for_scan(
+ &entry(partition(8), "DELETION_VECTORS"),
+ true,
+ true,
+ Some(&filter),
+ )
+ .unwrap());
+ }
+
#[tokio::test]
async fn test_skip_index_manifest_without_active_consumer() {
let table = Table::new(