Skip to content

Commit 21e5d49

Browse files
[branch-54] backport #22857 (Skip loading Parquet page index when row-group statistics already prove it cannot prune) (#23088)
backport #22857 to `branch-54` in anticipation of 54.1.0 release. DataFusion Comet encountered this issue while trying to upgrade to DF 54.0.0, so it would ease that upgrade for Comet if this were backported. This one had conflicts on cherry-pick, so I could use a sanity check on the review by comparing to the original PR. --------- Co-authored-by: Ratul Dawar <ratuldawar11@gmail.com>
1 parent aa59c66 commit 21e5d49

6 files changed

Lines changed: 435 additions & 72 deletions

File tree

datafusion-cli/src/main.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -613,9 +613,9 @@ mod tests {
613613
+-----------------------------------+-----------------+---------------------+------+------------------+
614614
| filename | file_size_bytes | metadata_size_bytes | hits | extra |
615615
+-----------------------------------+-----------------+---------------------+------+------------------+
616-
| alltypes_plain.parquet | 1851 | 8882 | 2 | page_index=false |
616+
| alltypes_plain.parquet | 1851 | 8882 | 1 | page_index=false |
617617
| alltypes_tiny_pages.parquet | 454233 | 269074 | 2 | page_index=true |
618-
| lz4_raw_compressed_larger.parquet | 380836 | 1339 | 2 | page_index=false |
618+
| lz4_raw_compressed_larger.parquet | 380836 | 1339 | 1 | page_index=false |
619619
+-----------------------------------+-----------------+---------------------+------+------------------+
620620
");
621621

@@ -644,9 +644,9 @@ mod tests {
644644
+-----------------------------------+-----------------+---------------------+------+------------------+
645645
| filename | file_size_bytes | metadata_size_bytes | hits | extra |
646646
+-----------------------------------+-----------------+---------------------+------+------------------+
647-
| alltypes_plain.parquet | 1851 | 8882 | 5 | page_index=false |
647+
| alltypes_plain.parquet | 1851 | 8882 | 4 | page_index=false |
648648
| alltypes_tiny_pages.parquet | 454233 | 269074 | 2 | page_index=true |
649-
| lz4_raw_compressed_larger.parquet | 380836 | 1339 | 3 | page_index=false |
649+
| lz4_raw_compressed_larger.parquet | 380836 | 1339 | 2 | page_index=false |
650650
+-----------------------------------+-----------------+---------------------+------+------------------+
651651
");
652652

datafusion/datasource-parquet/src/metadata.rs

Lines changed: 106 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
//! [`DFParquetMetadata`] for fetching Parquet file metadata, statistics
1919
//! and schema information.
2020
21+
use crate::file_format::ObjectStoreFetch;
2122
use crate::{Int96Coercer, apply_file_schema_type_coercions};
2223
use arrow::array::{Array, ArrayRef, BooleanArray};
2324
use arrow::compute::and;
@@ -43,8 +44,8 @@ use parquet::DecodeResult;
4344
use parquet::arrow::arrow_reader::statistics::StatisticsConverter;
4445
use parquet::arrow::{parquet_column, parquet_to_arrow_schema};
4546
use parquet::file::metadata::{
46-
PageIndexPolicy, ParquetMetaData, ParquetMetaDataPushDecoder, RowGroupMetaData,
47-
SortingColumn,
47+
PageIndexPolicy, ParquetMetaData, ParquetMetaDataPushDecoder, ParquetMetaDataReader,
48+
RowGroupMetaData, SortingColumn,
4849
};
4950
use parquet::schema::types::SchemaDescriptor;
5051
use std::any::Any;
@@ -70,6 +71,7 @@ pub struct DFParquetMetadata<'a> {
7071
metadata_size_hint: Option<usize>,
7172
decryption_properties: Option<Arc<FileDecryptionProperties>>,
7273
file_metadata_cache: Option<Arc<dyn FileMetadataCache>>,
74+
page_index_policy: Option<PageIndexPolicy>,
7375
/// timeunit to coerce INT96 timestamps to
7476
pub coerce_int96: Option<TimeUnit>,
7577
/// Optional timezone applied to INT96-coerced timestamps.
@@ -84,6 +86,7 @@ impl<'a> DFParquetMetadata<'a> {
8486
metadata_size_hint: None,
8587
decryption_properties: None,
8688
file_metadata_cache: None,
89+
page_index_policy: None,
8790
coerce_int96: None,
8891
coerce_int96_tz: None,
8992
}
@@ -113,6 +116,15 @@ impl<'a> DFParquetMetadata<'a> {
113116
self
114117
}
115118

119+
/// Sets the policy for loading parquet page index structures (column and offset indexes).
120+
pub fn with_page_index_policy(
121+
mut self,
122+
page_index_policy: Option<PageIndexPolicy>,
123+
) -> Self {
124+
self.page_index_policy = page_index_policy;
125+
self
126+
}
127+
116128
/// Set timeunit to coerce INT96 timestamps to
117129
pub fn with_coerce_int96(mut self, time_unit: Option<TimeUnit>) -> Self {
118130
self.coerce_int96 = time_unit;
@@ -127,9 +139,27 @@ impl<'a> DFParquetMetadata<'a> {
127139

128140
/// Fetch parquet metadata from the remote object store
129141
pub async fn fetch_metadata(&self) -> Result<Arc<ParquetMetaData>> {
130-
// implementation to fetch parquet metadata
142+
// fetch_metadata
143+
// │
144+
// ├─ cache_metadata = encryption check
145+
// ├─ page_index_policy = caller override OR default
146+
// │
147+
// ├─ CACHE HIT?
148+
// │ │
149+
// │ ├─ has index OR policy=Skip? → return cache
150+
// │ │
151+
// │ └─ else (footer only, wants index)
152+
// │ → load_page_index (index bytes only)
153+
// │ → cache_metadata() if allowed
154+
// │ → return
155+
// │
156+
// └─ CACHE MISS
157+
// → fetch_metadata_from_store(policy)
158+
// → cache_metadata() if allowed
159+
// → return
131160
let cache_metadata =
132161
!cfg!(feature = "parquet_encryption") || self.decryption_properties.is_none();
162+
let page_index_policy = self.effective_page_index_policy(cache_metadata);
133163

134164
if cache_metadata
135165
&& let Some(file_metadata_cache) = self.file_metadata_cache.as_ref()
@@ -140,9 +170,61 @@ impl<'a> DFParquetMetadata<'a> {
140170
.as_any()
141171
.downcast_ref::<CachedParquetMetaData>()
142172
{
143-
return Ok(Arc::clone(cached_parquet.parquet_metadata()));
173+
let cached_metadata = Arc::clone(cached_parquet.parquet_metadata());
174+
// Reuse the cache when it already has page index, or when the caller
175+
// asked to skip page index I/O (footer-only metadata is sufficient).
176+
if Self::metadata_has_page_index(cached_metadata.as_ref())
177+
|| page_index_policy == PageIndexPolicy::Skip
178+
{
179+
return Ok(cached_metadata);
180+
}
181+
let metadata =
182+
Self::load_page_index(self.store, self.object_meta, cached_metadata)
183+
.await?;
184+
if cache_metadata {
185+
self.cache_metadata(Arc::clone(&metadata)).await?;
186+
}
187+
return Ok(metadata);
188+
}
189+
190+
let metadata = self.fetch_metadata_from_store(page_index_policy).await?;
191+
if cache_metadata {
192+
self.cache_metadata(Arc::clone(&metadata)).await?;
193+
}
194+
Ok(metadata)
195+
}
196+
197+
fn effective_page_index_policy(&self, cache_metadata: bool) -> PageIndexPolicy {
198+
self.page_index_policy.unwrap_or_else(|| {
199+
if cache_metadata && self.file_metadata_cache.is_some() {
200+
PageIndexPolicy::Optional
201+
} else {
202+
PageIndexPolicy::Skip
203+
}
204+
})
205+
}
206+
207+
fn metadata_has_page_index(metadata: &ParquetMetaData) -> bool {
208+
metadata.column_index().is_some() && metadata.offset_index().is_some()
209+
}
210+
211+
async fn cache_metadata(&self, metadata: Arc<ParquetMetaData>) -> Result<()> {
212+
if let Some(file_metadata_cache) = &self.file_metadata_cache {
213+
file_metadata_cache.put(
214+
&self.object_meta.location,
215+
CachedFileMetadataEntry::new(
216+
self.object_meta.clone(),
217+
Arc::new(CachedParquetMetaData::new(metadata)),
218+
),
219+
);
144220
}
221+
Ok(())
222+
}
145223

224+
async fn fetch_metadata_from_store(
225+
&self,
226+
page_index_policy: PageIndexPolicy,
227+
) -> Result<Arc<ParquetMetaData>> {
146228
let file_size = self.object_meta.size;
147229
let mut decoder = ParquetMetaDataPushDecoder::try_new(file_size)
148230
.map_err(DataFusionError::from)?;
@@ -153,14 +235,8 @@ impl<'a> DFParquetMetadata<'a> {
153235
.with_file_decryption_properties(Some(Arc::clone(decryption_properties)));
154236
}
155237

156-
if cache_metadata && self.file_metadata_cache.is_some() {
157-
// Need to retrieve the entire metadata for the caching to be effective.
158-
decoder = decoder.with_page_index_policy(PageIndexPolicy::Optional);
159-
} else {
160-
decoder = decoder.with_page_index_policy(PageIndexPolicy::Skip);
161-
}
238+
decoder = decoder.with_page_index_policy(page_index_policy);
162239

163-
// If we have a size hint, prefetch that many bytes from the end of the file
164240
if let Some(hint) = self.metadata_size_hint {
165241
let prefetch_start = file_size.saturating_sub(hint as u64);
166242
let prefetch_range = prefetch_start..file_size;
@@ -199,19 +275,27 @@ impl<'a> DFParquetMetadata<'a> {
199275
}
200276
};
201277

202-
let metadata = Arc::new(metadata);
278+
Ok(Arc::new(metadata))
279+
}
203280

204-
if cache_metadata && let Some(file_metadata_cache) = &self.file_metadata_cache {
205-
file_metadata_cache.put(
206-
&self.object_meta.location,
207-
CachedFileMetadataEntry::new(
208-
self.object_meta.clone(),
209-
Arc::new(CachedParquetMetaData::new(Arc::clone(&metadata))),
210-
),
211-
);
281+
async fn load_page_index(
282+
store: &dyn ObjectStore,
283+
object_meta: &ObjectMeta,
284+
metadata: Arc<ParquetMetaData>,
285+
) -> Result<Arc<ParquetMetaData>> {
286+
if metadata.column_index().is_some() && metadata.offset_index().is_some() {
287+
return Ok(metadata);
212288
}
213-
214-
Ok(metadata)
289+
let metadata =
290+
Arc::try_unwrap(metadata).unwrap_or_else(|shared| (*shared).clone());
291+
let mut reader = ParquetMetaDataReader::new_with_metadata(metadata)
292+
.with_page_index_policy(PageIndexPolicy::Optional);
293+
let fetch = ObjectStoreFetch::new(store, object_meta);
294+
reader
295+
.load_page_index(fetch)
296+
.await
297+
.map_err(DataFusionError::from)?;
298+
Ok(Arc::new(reader.finish().map_err(DataFusionError::from)?))
215299
}
216300

217301
/// Read and parse the schema of the Parquet file

datafusion/datasource-parquet/src/metrics.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,23 @@ impl ParquetFileMetrics {
237237
.counter("page_index_pages_skipped_by_fully_matched", partition);
238238
count.add(n);
239239
}
240+
241+
/// Record that page index I/O was skipped because row-group statistics
242+
/// already proved page index could not prune further.
243+
pub(crate) fn add_page_index_load_skipped(
244+
metrics: &ExecutionPlanMetricsSet,
245+
partition: usize,
246+
filename: &str,
247+
n: usize,
248+
) {
249+
if n == 0 {
250+
return;
251+
}
252+
253+
let count = MetricBuilder::new(metrics)
254+
.with_new_label("filename", filename.to_string())
255+
.with_type(MetricType::Summary)
256+
.counter("page_index_load_skipped", partition);
257+
count.add(n);
258+
}
240259
}

0 commit comments

Comments
 (0)