-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Core: Enable column statistics filtering after planning #8803
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 1 commit
5bb425d
7a3effa
ae5975c
be9a11e
c8061f3
d0fcac2
9872f1f
f6279d0
32d2645
1922ae0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,9 +19,9 @@ | |
| package org.apache.iceberg; | ||
|
|
||
| import java.nio.ByteBuffer; | ||
| import java.util.Collection; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * Superinterface of {@link DataFile} and {@link DeleteFile} that exposes common methods. | ||
|
|
@@ -168,13 +168,14 @@ default Long fileSequenceNumber() { | |
|
|
||
| /** | ||
| * Copies this file with only specific column stats. Manifest readers can reuse file instances; | ||
| * use this method to copy data and only copy specific stats when collecting files. | ||
| * use this method to copy data and only copy specific stats when collecting files. If the | ||
|
pvary marked this conversation as resolved.
Outdated
|
||
| * columnsToKeepStats set is empty or <code>null</code>, then all column stats will be kept. | ||
| * | ||
| * @param statsToKeep the collection of the column ids for the columns which stats are kept | ||
| * @param columnsToKeepStats the set of the column ids for the columns which stats are kept. | ||
| * @return a copy of this data file, with stats lower bounds, upper bounds, value counts, null | ||
|
pvary marked this conversation as resolved.
Outdated
pvary marked this conversation as resolved.
Outdated
|
||
| * value counts, and nan value counts for only specific columns. | ||
| */ | ||
| default F copyWithSpecificStats(Collection<Integer> statsToKeep) { | ||
| default F copyWithStats(Set<Integer> columnsToKeepStats) { | ||
|
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. If we decide to do this rename, we should probably be consistent here as well. |
||
| throw new UnsupportedOperationException( | ||
| this.getClass().getName() + " doesn't implement copyWithSpecificStats"); | ||
|
pvary marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
@@ -191,26 +192,4 @@ default F copyWithSpecificStats(Collection<Integer> statsToKeep) { | |
| default F copy(boolean withStats) { | ||
| return withStats ? copy() : copyWithoutStats(); | ||
| } | ||
|
|
||
| /** | ||
| * Copies this file (potentially with or without specific column stats). Manifest readers can | ||
| * reuse file instances; use this method to copy data when collecting files from tasks. | ||
| * | ||
| * @param withStats Will copy this file without file stats if set to <code>false</code>. | ||
| * @param statsToKeep Will keep stats only for these columns. Not used if <code>withStats</code> | ||
| * is set to <code>false</code>. | ||
| * @return a copy of this data file. If "withStats" is set to <code>false</code> the file will not | ||
| * contain lower bounds, upper bounds, value counts, null value counts, or nan value counts. | ||
| * If "withStats" is set to <code>true</code> and the "statsToKeep" is not empty then only | ||
| * specific column stats will be kept. | ||
| */ | ||
| default F copy(boolean withStats, Collection<Integer> statsToKeep) { | ||
| if (withStats) { | ||
| return statsToKeep != null && !statsToKeep.isEmpty() | ||
| ? copyWithSpecificStats(statsToKeep) | ||
| : copy(); | ||
| } else { | ||
| return copyWithoutStats(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
| package org.apache.iceberg.util; | ||
|
|
||
| import java.util.Set; | ||
| import org.apache.iceberg.ContentFile; | ||
|
|
||
| public class ContentFileUtil { | ||
|
pvary marked this conversation as resolved.
|
||
| private ContentFileUtil() {} | ||
|
|
||
| /** | ||
| * Copies the {@link ContentFile} with the specific stat settings. | ||
| * | ||
| * @param file a generic data file to copy. | ||
| * @param withStats whether to keep any stats | ||
| * @param statsToKeep a set of column ids to keep stats. If empty or <code>null</code> then every | ||
| * column stat is kept. | ||
| * @return The copied file | ||
| */ | ||
| public static <F extends ContentFile<K>, K> K copy( | ||
| F file, boolean withStats, Set<Integer> statsToKeep) { | ||
|
pvary marked this conversation as resolved.
Outdated
|
||
| if (withStats) { | ||
| return statsToKeep != null && !statsToKeep.isEmpty() | ||
| ? file.copyWithStats(statsToKeep) | ||
| : file.copy(); | ||
| } else { | ||
| return file.copyWithoutStats(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,11 +21,12 @@ | |
| import java.io.Serializable; | ||
| import java.nio.ByteBuffer; | ||
| import java.util.Arrays; | ||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.function.Function; | ||
| import org.apache.avro.Schema; | ||
| import org.apache.avro.generic.IndexedRecord; | ||
| import org.apache.avro.specific.SpecificData; | ||
|
|
@@ -176,9 +177,10 @@ public PartitionData copy() { | |
| * | ||
| * @param toCopy a generic data file to copy. | ||
| * @param fullCopy whether to copy all fields or to drop column-level stats | ||
| * @param statsToKeep the collection of the column ids for the columns which stats are kept | ||
| * @param statsToKeep a set of column ids to keep stats. If empty or <code>null</code> then every | ||
| * column stat is kept. | ||
| */ | ||
| BaseFile(BaseFile<F> toCopy, boolean fullCopy, Collection<Integer> statsToKeep) { | ||
| BaseFile(BaseFile<F> toCopy, boolean fullCopy, Set<Integer> statsToKeep) { | ||
|
pvary marked this conversation as resolved.
Outdated
|
||
| this.fileOrdinal = toCopy.fileOrdinal; | ||
| this.partitionSpecId = toCopy.partitionSpecId; | ||
| this.content = toCopy.content; | ||
|
|
@@ -189,23 +191,24 @@ public PartitionData copy() { | |
| this.recordCount = toCopy.recordCount; | ||
| this.fileSizeInBytes = toCopy.fileSizeInBytes; | ||
| if (fullCopy) { | ||
| if (statsToKeep == null || statsToKeep.isEmpty()) { | ||
| this.columnSizes = SerializableMap.copyOf(toCopy.columnSizes); | ||
| this.valueCounts = SerializableMap.copyOf(toCopy.valueCounts); | ||
| this.nullValueCounts = SerializableMap.copyOf(toCopy.nullValueCounts); | ||
| this.nanValueCounts = SerializableMap.copyOf(toCopy.nanValueCounts); | ||
| this.lowerBounds = | ||
| SerializableByteBufferMap.wrap(SerializableMap.copyOf(toCopy.lowerBounds)); | ||
| this.upperBounds = | ||
| SerializableByteBufferMap.wrap(SerializableMap.copyOf(toCopy.upperBounds)); | ||
| } else { | ||
| this.columnSizes = filteredLongMap(toCopy.columnSizes, statsToKeep); | ||
| this.valueCounts = filteredLongMap(toCopy.valueCounts, statsToKeep); | ||
| this.nullValueCounts = filteredLongMap(toCopy.nullValueCounts, statsToKeep); | ||
| this.nanValueCounts = filteredLongMap(toCopy.nanValueCounts, statsToKeep); | ||
| this.lowerBounds = filteredByteBufferMap(toCopy.lowerBounds, statsToKeep); | ||
| this.upperBounds = filteredByteBufferMap(toCopy.upperBounds, statsToKeep); | ||
| } | ||
| this.columnSizes = | ||
| filterColumnsStats(toCopy.columnSizes, statsToKeep, SerializableMap::copyOf); | ||
| this.valueCounts = | ||
| filterColumnsStats(toCopy.valueCounts, statsToKeep, SerializableMap::copyOf); | ||
| this.nullValueCounts = | ||
| filterColumnsStats(toCopy.nullValueCounts, statsToKeep, SerializableMap::copyOf); | ||
| this.nanValueCounts = | ||
| filterColumnsStats(toCopy.nanValueCounts, statsToKeep, SerializableMap::copyOf); | ||
| this.lowerBounds = | ||
| filterColumnsStats( | ||
| toCopy.lowerBounds, | ||
| statsToKeep, | ||
| m -> SerializableByteBufferMap.wrap(SerializableMap.copyOf(m))); | ||
| this.upperBounds = | ||
| filterColumnsStats( | ||
| toCopy.upperBounds, | ||
| statsToKeep, | ||
| m -> SerializableByteBufferMap.wrap(SerializableMap.copyOf(m))); | ||
| } else { | ||
| this.columnSizes = null; | ||
| this.valueCounts = null; | ||
|
|
@@ -518,38 +521,27 @@ private static Map<Integer, ByteBuffer> toReadableByteBufferMap(Map<Integer, Byt | |
| } | ||
| } | ||
|
|
||
| private static Map<Integer, Long> filteredLongMap( | ||
| Map<Integer, Long> map, Collection<Integer> columnIds) { | ||
| if (map == null) { | ||
| return null; | ||
| private static <TypeT> Map<Integer, TypeT> filterColumnsStats( | ||
|
stevenzwu marked this conversation as resolved.
Outdated
|
||
| Map<Integer, TypeT> map, | ||
| Set<Integer> columnIds, | ||
| Function<Map<Integer, TypeT>, Map<Integer, TypeT>> copyFunction) { | ||
| if (columnIds == null || columnIds.isEmpty()) { | ||
| return copyFunction.apply(map); | ||
| } | ||
|
|
||
| Map<Integer, Long> filtered = Maps.newHashMapWithExpectedSize(columnIds.size()); | ||
| for (Integer columnId : columnIds) { | ||
| Long value = map.get(columnId); | ||
| if (value != null) { | ||
| filtered.put(columnId, value); | ||
| } | ||
| } | ||
|
|
||
| return SerializableMap.copyOf(filtered); | ||
| } | ||
|
|
||
| private static Map<Integer, ByteBuffer> filteredByteBufferMap( | ||
| Map<Integer, ByteBuffer> map, Collection<Integer> columnIds) { | ||
| if (map == null) { | ||
|
stevenzwu marked this conversation as resolved.
Outdated
|
||
| return null; | ||
| } | ||
|
|
||
| Map<Integer, ByteBuffer> filtered = Maps.newHashMapWithExpectedSize(columnIds.size()); | ||
| Map<Integer, TypeT> filtered = Maps.newHashMapWithExpectedSize(columnIds.size()); | ||
|
stevenzwu marked this conversation as resolved.
Outdated
|
||
| for (Integer columnId : columnIds) { | ||
| ByteBuffer value = map.get(columnId); | ||
| TypeT value = map.get(columnId); | ||
| if (value != null) { | ||
|
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. is it necessary to check null value?
Contributor
Author
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. Yes, I found this in some tests, that the map could contain null values
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. If there are null values in lower_bound or upper_bound, why do we want to filter them out? shouldn't we keep the same behavior when copying stats for selected columns.
Contributor
Author
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. My previous answer was not clear enough. Let me rephrase: "we can have maps where the columnId is not contained in the key list". In this case the
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. sure. but could lower_bound and upper_bound have null value? how do we distinguish btw these two scenario? e.g. here is the description for lower_bound
Contributor
Author
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. I did some testing with Parquet files:
Notice, that the size of the
Notice, that the size of the So I think the actual phrasing of the documentation is just not clean enough, as the map does not contain
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. thanks for the unit test code. I am still not comfortable of making the assumption as the Iceberg spec clearly said null stats value is possible. what if Trino parquet writer produces min/max stats with null value. As least, there is a mismatch in the Parquet writer implementation and the spec. Unless we are going to clarify/update the spec, I feel it is safer to do Maybe @RussellSpitzer and @rdblue can chime in here.
Contributor
Author
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. Changed to ensure that even Still not sure that this is necessary, but I will be on PTO next week, so wanted to have it here as a possibility
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. thanks. double fetching is probably ok since the columnsToKeepStats is likely not big and Map get is a O(1) anyway. The alternative is to start a discussion and revisit the spec or Parquet implementation. |
||
| filtered.put(columnId, value); | ||
| } | ||
| } | ||
|
|
||
| return SerializableMap.copyOf(filtered); | ||
| return copyFunction.apply(filtered); | ||
| } | ||
|
|
||
| @Override | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.