-
Notifications
You must be signed in to change notification settings - Fork 3k
Parquet: Support Page Skipping in Iceberg Parquet Reader #1566
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,7 @@ | |
| import org.apache.iceberg.encryption.EncryptionKeyMetadata; | ||
| import org.apache.iceberg.exceptions.RuntimeIOException; | ||
| import org.apache.iceberg.expressions.Expression; | ||
| import org.apache.iceberg.expressions.Expressions; | ||
| import org.apache.iceberg.hadoop.HadoopInputFile; | ||
| import org.apache.iceberg.hadoop.HadoopOutputFile; | ||
| import org.apache.iceberg.io.CloseableIterable; | ||
|
|
@@ -582,6 +583,12 @@ public <D> CloseableIterable<D> build() { | |
| optionsBuilder = ParquetReadOptions.builder(); | ||
| } | ||
|
|
||
| if (filter != null && !filter.equals(Expressions.alwaysTrue()) && | ||
| ParquetFilters.isSupportedFilter(filter)) { | ||
| optionsBuilder.useRecordFilter(filterRecords); | ||
| optionsBuilder.withRecordFilter(ParquetFilters.convert(getSchemaFromFile(), filter, caseSensitive)); | ||
| } | ||
|
|
||
| for (Map.Entry<String, String> entry : properties.entrySet()) { | ||
| optionsBuilder.set(entry.getKey(), entry.getValue()); | ||
| } | ||
|
|
@@ -623,17 +630,10 @@ public <D> CloseableIterable<D> build() { | |
| if (filter != null) { | ||
| // TODO: should not need to get the schema to push down before opening the file. | ||
| // Parquet should allow setting a filter inside its read support | ||
| MessageType type; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment above sounds like another possible reason why we wanted to reimplement filters. Tailoring the filter to each file is difficult, compared to evaluating the same filter for a file. If I remember correctly, the need to tailor the filter for the file is because we use id-based column resolution. So the file might contain |
||
| try (ParquetFileReader schemaReader = ParquetFileReader.open(ParquetIO.file(file))) { | ||
| type = schemaReader.getFileMetaData().getSchema(); | ||
| } catch (IOException e) { | ||
| throw new RuntimeIOException(e); | ||
| } | ||
| Schema fileSchema = ParquetSchemaUtil.convert(type); | ||
| builder.useStatsFilter() | ||
| .useDictionaryFilter() | ||
| .useRecordFilter(filterRecords) | ||
| .withFilter(ParquetFilters.convert(fileSchema, filter, caseSensitive)); | ||
| .withFilter(ParquetFilters.convert(getSchemaFromFile(), filter, caseSensitive)); | ||
| } else { | ||
| // turn off filtering | ||
| builder.useStatsFilter(false) | ||
|
|
@@ -655,6 +655,16 @@ public <D> CloseableIterable<D> build() { | |
|
|
||
| return new ParquetIterable<>(builder); | ||
| } | ||
|
|
||
| private Schema getSchemaFromFile() { | ||
| MessageType type; | ||
| try (ParquetFileReader schemaReader = ParquetFileReader.open(ParquetIO.file(file))) { | ||
| type = schemaReader.getFileMetaData().getSchema(); | ||
| } catch (IOException e) { | ||
| throw new RuntimeIOException(e); | ||
| } | ||
| return ParquetSchemaUtil.convert(type); | ||
| } | ||
| } | ||
|
|
||
| private static class ParquetReadBuilder<T> extends ParquetReader.Builder<T> { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,7 +54,6 @@ class ReadConf<T> { | |
| private final ParquetValueReader<T> model; | ||
| private final VectorizedReader<T> vectorizedModel; | ||
| private final List<BlockMetaData> rowGroups; | ||
| private final boolean[] shouldSkip; | ||
| private final long totalValues; | ||
| private final boolean reuseContainers; | ||
| private final Integer batchSize; | ||
|
|
@@ -85,34 +84,28 @@ class ReadConf<T> { | |
| this.projection = ParquetSchemaUtil.pruneColumnsFallback(fileSchema, expectedSchema); | ||
| } | ||
|
|
||
| // ParquetFileReader has filters(stats, dictionary and future bloomfilter) in the constructor, | ||
| // so getRowGroups returns filtered row groups | ||
| this.rowGroups = reader.getRowGroups(); | ||
| this.shouldSkip = new boolean[rowGroups.size()]; | ||
|
|
||
| // Fetch all row groups starting positions to compute the row offsets of the filtered row groups | ||
| Map<Long, Long> offsetToStartPos = generateOffsetToStartPos(); | ||
| this.startRowPositions = new long[rowGroups.size()]; | ||
|
|
||
| ParquetMetricsRowGroupFilter statsFilter = null; | ||
| ParquetDictionaryRowGroupFilter dictFilter = null; | ||
| if (filter != null) { | ||
| statsFilter = new ParquetMetricsRowGroupFilter(expectedSchema, filter, caseSensitive); | ||
| dictFilter = new ParquetDictionaryRowGroupFilter(expectedSchema, filter, caseSensitive); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR removes support for pushing down filters like |
||
|
|
||
| long computedTotalValues = 0L; | ||
| for (int i = 0; i < shouldSkip.length; i += 1) { | ||
| // If a row group has 0 counts after filtering, it won't be added to rowGroups | ||
| for (int i = 0; i < rowGroups.size(); i += 1) { | ||
| BlockMetaData rowGroup = rowGroups.get(i); | ||
| startRowPositions[i] = offsetToStartPos.get(rowGroup.getStartingPos()); | ||
| boolean shouldRead = filter == null || ( | ||
| statsFilter.shouldRead(typeWithIds, rowGroup) && | ||
| dictFilter.shouldRead(typeWithIds, rowGroup, reader.getDictionaryReader(rowGroup))); | ||
| this.shouldSkip[i] = !shouldRead; | ||
| if (shouldRead) { | ||
| computedTotalValues += rowGroup.getRowCount(); | ||
| } | ||
| computedTotalValues += rowGroup.getRowCount(); | ||
| } | ||
|
|
||
| this.totalValues = computedTotalValues; | ||
| // Because of the issue of PARQUET-1901, we cannot blindly call getFilteredRecordCount() | ||
| if (filter != null) { | ||
| this.totalValues = reader.getFilteredRecordCount(); | ||
| } else { | ||
| this.totalValues = computedTotalValues; | ||
| } | ||
| if (readerFunc != null) { | ||
| this.model = (ParquetValueReader<T>) readerFunc.apply(typeWithIds); | ||
| this.vectorizedModel = null; | ||
|
|
@@ -134,7 +127,6 @@ private ReadConf(ReadConf<T> toCopy) { | |
| this.projection = toCopy.projection; | ||
| this.model = toCopy.model; | ||
| this.rowGroups = toCopy.rowGroups; | ||
| this.shouldSkip = toCopy.shouldSkip; | ||
| this.totalValues = toCopy.totalValues; | ||
| this.reuseContainers = toCopy.reuseContainers; | ||
| this.batchSize = toCopy.batchSize; | ||
|
|
@@ -162,10 +154,6 @@ VectorizedReader<T> vectorizedModel() { | |
| return vectorizedModel; | ||
| } | ||
|
|
||
| boolean[] shouldSkip() { | ||
| return shouldSkip; | ||
| } | ||
|
|
||
| private Map<Long, Long> generateOffsetToStartPos() { | ||
| try (ParquetFileReader fileReader = newReader(file, ParquetReadOptions.builder().build())) { | ||
| Map<Long, Long> offsetToStartPos = new HashMap<>(); | ||
|
|
@@ -208,6 +196,10 @@ ReadConf<T> copy() { | |
| return new ReadConf<>(this); | ||
| } | ||
|
|
||
| boolean hasRecordFilter() { | ||
| return options.getRecordFilter() != null; | ||
| } | ||
|
|
||
| private static ParquetFileReader newReader(InputFile file, ParquetReadOptions options) { | ||
| try { | ||
| return ParquetFileReader.open(ParquetIO.file(file), options); | ||
|
|
@@ -221,16 +213,12 @@ private List<Map<ColumnPath, ColumnChunkMetaData>> getColumnChunkMetadataForRowG | |
| .map(columnDescriptor -> ColumnPath.get(columnDescriptor.getPath())).collect(Collectors.toSet()); | ||
| ImmutableList.Builder<Map<ColumnPath, ColumnChunkMetaData>> listBuilder = ImmutableList.builder(); | ||
| for (int i = 0; i < rowGroups.size(); i++) { | ||
| if (!shouldSkip[i]) { | ||
| BlockMetaData blockMetaData = rowGroups.get(i); | ||
| ImmutableMap.Builder<ColumnPath, ColumnChunkMetaData> mapBuilder = ImmutableMap.builder(); | ||
| blockMetaData.getColumns().stream() | ||
| .filter(columnChunkMetaData -> projectedColumns.contains(columnChunkMetaData.getPath())) | ||
| .forEach(columnChunkMetaData -> mapBuilder.put(columnChunkMetaData.getPath(), columnChunkMetaData)); | ||
| listBuilder.add(mapBuilder.build()); | ||
| } else { | ||
| listBuilder.add(ImmutableMap.of()); | ||
| } | ||
| BlockMetaData blockMetaData = rowGroups.get(i); | ||
| ImmutableMap.Builder<ColumnPath, ColumnChunkMetaData> mapBuilder = ImmutableMap.builder(); | ||
| blockMetaData.getColumns().stream() | ||
| .filter(columnChunkMetaData -> projectedColumns.contains(columnChunkMetaData.getPath())) | ||
| .forEach(columnChunkMetaData -> mapBuilder.put(columnChunkMetaData.getPath(), columnChunkMetaData)); | ||
| listBuilder.add(mapBuilder.build()); | ||
| } | ||
| return listBuilder.build(); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.