dongjoon-hyun commented on code in PR #59324:
URL: https://github.com/apache/spark/pull/59324#discussion_r4264399042
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -1976,37 +1969,20 @@ object SQLConf {
val PARQUET_STORAGE_FILTER_PUSHDOWN_ENABLED =
buildConf("spark.sql.parquet.storageFilterPushdown.enabled")
.doc("If true, the vectorized reader of the V1 Parquet file source may
apply a runtime " +
- "storage filter, such as a bloom filter from join runtime filtering,
while it reads. " +
- "It reads the columns the " +
- "filter needs first, evaluates the filter per row, and then reads the
remaining columns " +
- "only for the rows that survived. This is a planning-time decision. " +
- "A filter that is attached also stays in the post-scan filter, the way
a pushed data " +
- "filter does, so the reader is free to stop applying it wherever doing
so would cost " +
- "more than it saves, and the answer does not change. In a scan that
returns columnar " +
- "batches, the post-scan filter skips it on the rows the reader applied
it to, which the " +
- "reader marks in a column named _tmp_storage_filter_checked. A table
with a column of " +
- "that name is not offered storage filters, and a file holding one
outside the table's " +
- "schema is not supported. A row group where the reader stops applying
the filter reads " +
- "its key columns twice, which is slower than not pushing the filter. "
+
- "Under spark.sql.files.ignoreCorruptFiles the answer can change,
because this reader " +
- "reads different pages in a different order than a plain read. Which
rows survive a " +
- "corrupt page can then differ from a plain read, in either direction.
" +
- "Returning only the surviving rows of a row group relies on the
Parquet page index, so a " +
- "file whose page index is wrong can pair a row's key with another
row's values, or " +
- "return rows the filter rejects. Setting " +
- "parquet.filter.columnindex.enabled to false makes the reader fall
back to skipping " +
- "whole row groups in which the filter rejects every row. A row group
with a surviving " +
- "row then reads its key columns twice, and the post-scan filter
evaluates the filter " +
- "on each of its rows. A file without a page index falls back the same
" +
- "way. It gains only where a whole row group has no surviving row, a
bloom's false " +
- "positives included, and every other row group reads its key columns
twice. A read " +
- "with pushed data filters already relies on the page index, and that
conf turns it off " +
- "there too. A read with no pushed data filter relies on it only with
this feature. " +
- "Note that the surviving key values of a whole row group are buffered
before that row " +
- "group's first batch is produced. Each reader holds them and their row
ranges for one " +
- "row group at a time, up to " +
- s"${PARQUET_STORAGE_FILTER_PUSHDOWN_MAX_SPLICED_ROW_GROUP_BYTES.key}.
Like the reader's " +
- "own column vectors, they are not tracked by Spark's memory manager.")
+ "bloom filter from join runtime filtering while it reads, and skip the
pages and row " +
+ "groups of the other columns that hold no matching rows. Only
ParquetFileFormat itself " +
+ "is supported, not its subclasses. A table with a column named " +
+ "_tmp_storage_filter_checked is not offered storage filters, and a
file holding one " +
+ "outside the table's schema is not supported. The filter also stays in
the post-scan " +
+ "filter, so the answer does not change, with two exceptions. Under " +
+ "spark.sql.files.ignoreCorruptFiles the rows returned from a corrupt
file can differ. A " +
+ "file with a wrong Parquet page index can return wrong rows. Setting "
+
+ "parquet.filter.columnindex.enabled to false stops relying on the page
index, and also " +
+ "turns off page skipping for pushed data filters. Without a page
index, as in the files " +
+ "pyarrow's write_table writes by default, or with that conf set to
false, the reader can " +
+ "only skip whole row groups, and it reads the key columns twice in the
others. It can be " +
Review Comment:
**2. After the reorder, "It can be slower" binds to the no-page-index case**
Moving the two unsupported cases up for round-1 comment 4 put this sentence
right after the no-page-index one:
> ... the reader can only skip whole row groups, and it reads the key
columns twice in the others. It can be slower when the matching rows are spread
across most pages.
"It" now reads as the reader without a page index. There, how the matches
spread across pages does not matter, since the first match in a row group gives
the filter up (`if (!canNarrowRowGroup) return null`). The caveat is about the
page-indexed case: when the matches touch most pages, phase 2 skips almost
nothing, while phase 1 still pays the per-row evaluation and the splicing. A
user whose files have a page index (parquet-mr 1.11 and later) would read the
only general performance caveat as not applying to them.
Maybe "Even with a page index, it can be slower when ..."? Moving the
sentence instead would leave the buffering sentence right after the
no-page-index one, a case where nothing is buffered.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -1976,37 +1969,20 @@ object SQLConf {
val PARQUET_STORAGE_FILTER_PUSHDOWN_ENABLED =
buildConf("spark.sql.parquet.storageFilterPushdown.enabled")
.doc("If true, the vectorized reader of the V1 Parquet file source may
apply a runtime " +
- "storage filter, such as a bloom filter from join runtime filtering,
while it reads. " +
- "It reads the columns the " +
- "filter needs first, evaluates the filter per row, and then reads the
remaining columns " +
- "only for the rows that survived. This is a planning-time decision. " +
- "A filter that is attached also stays in the post-scan filter, the way
a pushed data " +
- "filter does, so the reader is free to stop applying it wherever doing
so would cost " +
- "more than it saves, and the answer does not change. In a scan that
returns columnar " +
- "batches, the post-scan filter skips it on the rows the reader applied
it to, which the " +
- "reader marks in a column named _tmp_storage_filter_checked. A table
with a column of " +
- "that name is not offered storage filters, and a file holding one
outside the table's " +
- "schema is not supported. A row group where the reader stops applying
the filter reads " +
- "its key columns twice, which is slower than not pushing the filter. "
+
- "Under spark.sql.files.ignoreCorruptFiles the answer can change,
because this reader " +
- "reads different pages in a different order than a plain read. Which
rows survive a " +
- "corrupt page can then differ from a plain read, in either direction.
" +
- "Returning only the surviving rows of a row group relies on the
Parquet page index, so a " +
- "file whose page index is wrong can pair a row's key with another
row's values, or " +
- "return rows the filter rejects. Setting " +
- "parquet.filter.columnindex.enabled to false makes the reader fall
back to skipping " +
- "whole row groups in which the filter rejects every row. A row group
with a surviving " +
- "row then reads its key columns twice, and the post-scan filter
evaluates the filter " +
- "on each of its rows. A file without a page index falls back the same
" +
- "way. It gains only where a whole row group has no surviving row, a
bloom's false " +
- "positives included, and every other row group reads its key columns
twice. A read " +
- "with pushed data filters already relies on the page index, and that
conf turns it off " +
- "there too. A read with no pushed data filter relies on it only with
this feature. " +
- "Note that the surviving key values of a whole row group are buffered
before that row " +
- "group's first batch is produced. Each reader holds them and their row
ranges for one " +
- "row group at a time, up to " +
- s"${PARQUET_STORAGE_FILTER_PUSHDOWN_MAX_SPLICED_ROW_GROUP_BYTES.key}.
Like the reader's " +
- "own column vectors, they are not tracked by Spark's memory manager.")
+ "bloom filter from join runtime filtering while it reads, and skip the
pages and row " +
+ "groups of the other columns that hold no matching rows. Only
ParquetFileFormat itself " +
+ "is supported, not its subclasses. A table with a column named " +
+ "_tmp_storage_filter_checked is not offered storage filters, and a
file holding one " +
+ "outside the table's schema is not supported. The filter also stays in
the post-scan " +
+ "filter, so the answer does not change, with two exceptions. Under " +
+ "spark.sql.files.ignoreCorruptFiles the rows returned from a corrupt
file can differ. A " +
+ "file with a wrong Parquet page index can return wrong rows. Setting "
+
Review Comment:
**1. The conf doc again drops that bloom-only scans rely on the page index
only with this feature**
Round-1 comment 1 quoted only the first half of this pair from the base, and
that half is back. The second half is still gone:
> A read with no pushed data filter relies on it only with this feature.
That sentence was the agreed doc fix in #58895, twice: for [the OffsetIndex
thread](https://github.com/apache/spark/pull/58895#discussion_r4108708237), and
again in [round
7](https://github.com/apache/spark/pull/58895#discussion_r4148902141), where it
was restored after the round-6 rewrite had dropped it.
It still holds. A plain read with no pushed data filter, or only an inferred
`IS NOT NULL` that prunes no page, never consults the offset index, since
`readNextFilteredRowGroup` falls back to `readNextRowGroup`. With this feature,
phase 2 reads the survivors through the offset index
(`readFilteredRowGroup(blockIdx, finalRanges)`) and marks them checked. So a
file whose page index is wrong can start returning wrong rows from a query that
has always been right. Right after "also turns off page skipping for pushed
data filters", the hazard reads as one that the user's existing reads already
carry, so that user has no reason to apply the workaround.
Could we add it back, e.g. "A read with no pushed data filter relies on the
page index only with this feature."?
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -1976,37 +1969,20 @@ object SQLConf {
val PARQUET_STORAGE_FILTER_PUSHDOWN_ENABLED =
buildConf("spark.sql.parquet.storageFilterPushdown.enabled")
.doc("If true, the vectorized reader of the V1 Parquet file source may
apply a runtime " +
- "storage filter, such as a bloom filter from join runtime filtering,
while it reads. " +
- "It reads the columns the " +
- "filter needs first, evaluates the filter per row, and then reads the
remaining columns " +
- "only for the rows that survived. This is a planning-time decision. " +
- "A filter that is attached also stays in the post-scan filter, the way
a pushed data " +
- "filter does, so the reader is free to stop applying it wherever doing
so would cost " +
- "more than it saves, and the answer does not change. In a scan that
returns columnar " +
- "batches, the post-scan filter skips it on the rows the reader applied
it to, which the " +
- "reader marks in a column named _tmp_storage_filter_checked. A table
with a column of " +
- "that name is not offered storage filters, and a file holding one
outside the table's " +
- "schema is not supported. A row group where the reader stops applying
the filter reads " +
- "its key columns twice, which is slower than not pushing the filter. "
+
- "Under spark.sql.files.ignoreCorruptFiles the answer can change,
because this reader " +
- "reads different pages in a different order than a plain read. Which
rows survive a " +
- "corrupt page can then differ from a plain read, in either direction.
" +
- "Returning only the surviving rows of a row group relies on the
Parquet page index, so a " +
- "file whose page index is wrong can pair a row's key with another
row's values, or " +
- "return rows the filter rejects. Setting " +
- "parquet.filter.columnindex.enabled to false makes the reader fall
back to skipping " +
- "whole row groups in which the filter rejects every row. A row group
with a surviving " +
- "row then reads its key columns twice, and the post-scan filter
evaluates the filter " +
- "on each of its rows. A file without a page index falls back the same
" +
- "way. It gains only where a whole row group has no surviving row, a
bloom's false " +
- "positives included, and every other row group reads its key columns
twice. A read " +
- "with pushed data filters already relies on the page index, and that
conf turns it off " +
- "there too. A read with no pushed data filter relies on it only with
this feature. " +
- "Note that the surviving key values of a whole row group are buffered
before that row " +
- "group's first batch is produced. Each reader holds them and their row
ranges for one " +
- "row group at a time, up to " +
- s"${PARQUET_STORAGE_FILTER_PUSHDOWN_MAX_SPLICED_ROW_GROUP_BYTES.key}.
Like the reader's " +
- "own column vectors, they are not tracked by Spark's memory manager.")
+ "bloom filter from join runtime filtering while it reads, and skip the
pages and row " +
+ "groups of the other columns that hold no matching rows. Only
ParquetFileFormat itself " +
+ "is supported, not its subclasses. A table with a column named " +
+ "_tmp_storage_filter_checked is not offered storage filters, and a
file holding one " +
+ "outside the table's schema is not supported. The filter also stays in
the post-scan " +
+ "filter, so the answer does not change, with two exceptions. Under " +
+ "spark.sql.files.ignoreCorruptFiles the rows returned from a corrupt
file can differ. A " +
+ "file with a wrong Parquet page index can return wrong rows. Setting "
+
+ "parquet.filter.columnindex.enabled to false stops relying on the page
index, and also " +
+ "turns off page skipping for pushed data filters. Without a page
index, as in the files " +
+ "pyarrow's write_table writes by default, or with that conf set to
false, the reader can " +
+ "only skip whole row groups, and it reads the key columns twice in the
others. It can be " +
+ "slower when the matching rows are spread across most pages. The
matching key values of " +
+ "a row group are buffered while it is read, outside Spark's memory
manager.")
Review Comment:
**3. The conf doc no longer bounds the buffer it says is outside the memory
manager**
This doc is based on [viirya's
text](https://github.com/apache/spark/pull/58895#issuecomment-6064730845),
which ends "Matching key values are buffered per row group, up to
spark.sql.parquet.storageFilterPushdown.maxSplicedRowGroupBytes, outside
Spark's memory manager." The bound was dropped here, and so was the base's
"before that row group's first batch is produced".
`evaluateStorageFilter` buffers every survivor of a row group, and its row
ranges, before `installRowGroup`, and gives splicing up only past `splicedBytes
+ rangeBytes > cap`, 64MB by default. That conf is internal, so `SET -v` and
the generated docs show neither the knob nor its default. "buffered while it is
read" sounds like a batch-sized buffer, but it can be 64MB per running task,
which comes out of `spark.executor.memoryOverhead` when off heap.
Could we keep the bound, e.g. "The matching key values of a whole row group
are buffered before its first batch, up to 64MB per reader by default, outside
Spark's memory manager."? Naming the internal conf, as the base did, would work
too.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -1976,37 +1969,20 @@ object SQLConf {
val PARQUET_STORAGE_FILTER_PUSHDOWN_ENABLED =
buildConf("spark.sql.parquet.storageFilterPushdown.enabled")
.doc("If true, the vectorized reader of the V1 Parquet file source may
apply a runtime " +
- "storage filter, such as a bloom filter from join runtime filtering,
while it reads. " +
- "It reads the columns the " +
- "filter needs first, evaluates the filter per row, and then reads the
remaining columns " +
- "only for the rows that survived. This is a planning-time decision. " +
- "A filter that is attached also stays in the post-scan filter, the way
a pushed data " +
- "filter does, so the reader is free to stop applying it wherever doing
so would cost " +
- "more than it saves, and the answer does not change. In a scan that
returns columnar " +
- "batches, the post-scan filter skips it on the rows the reader applied
it to, which the " +
- "reader marks in a column named _tmp_storage_filter_checked. A table
with a column of " +
- "that name is not offered storage filters, and a file holding one
outside the table's " +
- "schema is not supported. A row group where the reader stops applying
the filter reads " +
- "its key columns twice, which is slower than not pushing the filter. "
+
- "Under spark.sql.files.ignoreCorruptFiles the answer can change,
because this reader " +
- "reads different pages in a different order than a plain read. Which
rows survive a " +
- "corrupt page can then differ from a plain read, in either direction.
" +
- "Returning only the surviving rows of a row group relies on the
Parquet page index, so a " +
- "file whose page index is wrong can pair a row's key with another
row's values, or " +
- "return rows the filter rejects. Setting " +
- "parquet.filter.columnindex.enabled to false makes the reader fall
back to skipping " +
- "whole row groups in which the filter rejects every row. A row group
with a surviving " +
- "row then reads its key columns twice, and the post-scan filter
evaluates the filter " +
- "on each of its rows. A file without a page index falls back the same
" +
- "way. It gains only where a whole row group has no surviving row, a
bloom's false " +
- "positives included, and every other row group reads its key columns
twice. A read " +
- "with pushed data filters already relies on the page index, and that
conf turns it off " +
- "there too. A read with no pushed data filter relies on it only with
this feature. " +
- "Note that the surviving key values of a whole row group are buffered
before that row " +
- "group's first batch is produced. Each reader holds them and their row
ranges for one " +
- "row group at a time, up to " +
- s"${PARQUET_STORAGE_FILTER_PUSHDOWN_MAX_SPLICED_ROW_GROUP_BYTES.key}.
Like the reader's " +
- "own column vectors, they are not tracked by Spark's memory manager.")
+ "bloom filter from join runtime filtering while it reads, and skip the
pages and row " +
+ "groups of the other columns that hold no matching rows. Only
ParquetFileFormat itself " +
+ "is supported, not its subclasses. A table with a column named " +
+ "_tmp_storage_filter_checked is not offered storage filters, and a
file holding one " +
+ "outside the table's schema is not supported. The filter also stays in
the post-scan " +
+ "filter, so the answer does not change, with two exceptions. Under " +
+ "spark.sql.files.ignoreCorruptFiles the rows returned from a corrupt
file can differ. A " +
+ "file with a wrong Parquet page index can return wrong rows. Setting "
+
+ "parquet.filter.columnindex.enabled to false stops relying on the page
index, and also " +
Review Comment:
**4. The doc does not say where `parquet.filter.columnindex.enabled` has to
be set**
This is parquet's Hadoop key, not a Spark conf. The configuration docs say
runtime SQL confs can be set in the config file or with `--conf`, but both drop
a key that does not start with `spark.`
(`SparkSubmitArguments.ignoreNonSparkProperties`, with only a WARN). A user who
sets it there keeps reading the bad page index.
It works with `SET` or `spark.conf.set`, as a read option, which also keeps
the side effect on pushed data filters to one scan, or at launch as
`spark.hadoop.parquet.filter.columnindex.enabled`. Other public docs say where
such keys go, e.g. `parquet.compression` "in the table-specific
options/properties". Maybe one clause, e.g. "..., in the session or as a read
option, ..."?
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetStorageFilter.scala:
##########
@@ -264,39 +217,26 @@ object ParquetStorageFilter {
}
/**
- * Rethrows an error the reader met while applying a storage filter, if it
must not fall back on
- * it by giving the filter up. `fromRead` says the error came from decoding
the key pages rather
- * than from evaluating the filter. The checks run in the executor's own
order:
- * - a fatal error goes out as it is, kill or no kill.
- * - a pending task kill goes out wrapped when the error is one the
executor's kill case takes,
- * see [[throwIfKilled]]. The error may be the kill's interrupt surfacing
through a UDF that
- * wrapped it. `PythonRunner` reads a kill off the task context in the
same way.
- * - a fatal error deeper in the cause chain, found by
`Executor.isFatalError` as deep as the
- * executor would look had a plain plan raised `e`, goes out as itself,
so the executor finds
- * it at the top whatever wraps it on the way.
+ * Rethrows an error the reader met while applying a storage filter, unless
it may give the filter
+ * up on it. `fromRead` says the error came from decoding the key pages. In
the executor's order:
+ * - a fatal error goes out as it is;
+ * - a pending task kill goes out wrapped, see [[throwIfKilled]], since the
error may be the
Review Comment:
**6. The kill bullet lost its condition**
> a pending task kill goes out wrapped, see [[throwIfKilled]], ...
and below, "An `InternalError` among those goes out in an `UnknownError`".
Neither holds for an error the executor's kill case does not take. With
`fromRead = true`, a codec's `InternalError` under a pending kill skips L249 as
`corrupt`, and `throwIfKilled(e)` rethrows it bare at L291 (`case other =>
throw other`), as a plain read would. The base said "goes out wrapped when the
error is one the executor's kill case takes", and `throwIfKilled`'s doc gave a
codec's `InternalError` and a native codec's `UnsatisfiedLinkError` as
examples. Aligning the code with the current text, wrapping always, would let
the executor report a fatal error as a kill and run on, which L228-229 rule out.
Maybe "a pending task kill goes out wrapped when the executor's kill case
takes the error, see [[throwIfKilled]], ..."?
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -117,25 +98,17 @@ public class LateMaterializationParquetRecordReader
extends VectorizedParquetRec
SparkLoggerFactory.getLogger(LateMaterializationParquetRecordReader.class);
/**
- * What one buffered variable-length value costs the vector that holds it,
as a multiple of the
- * value's own bytes. Those bytes go in the byte child, with one null byte
per element of that
- * child, and up to as much again because {@link
WritableColumnVector#reserve} doubles the child
- * when it grows, by default. A factor rather than an exact figure, so with
that default this
- * half of the charge over-counts rather than under-counts, since nothing
above this buffer can
- * spill it. A {@code hugeVectorReserveRatio} above 2 makes it under-count
for a child past
- * {@code hugeVectorThreshold}. That threshold is off by default. A
fixed-width key needs no
- * factor, since its accumulator is allocated at capacity and never grows.
+ * What one buffered variable-length value costs, as a multiple of its own
bytes. The byte child
+ * holds a null byte per byte, and {@link WritableColumnVector#reserve}
doubles it when it grows.
+ * It over-counts on purpose, since nothing can spill this buffer. That
holds at the default
+ * growth. A {@code hugeVectorReserveRatio} above 2 makes it under-count past
+ * {@code hugeVectorThreshold}, which is off by default.
*/
private static final long VARIABLE_LENGTH_BYTES_FACTOR = 4L;
/**
- * What one surviving row range costs to hold, which is the half of the
budget a row group
- * buffering nothing still pays. Scattered survivors make one range each,
and phase 2 needs the
- * whole set to select its pages. One set is held, which every column reader
of the row group
- * walks with a cursor of its own. The figure is parquet's {@code
RowRanges.Range}, two longs,
- * plus its object header and a list slot. That is what is held. At the end
of phase 1 the
- * builder's list can have up to half as many slots again, and {@code
build()} copies it, so the
- * peak is somewhat higher.
+ * What one surviving row range costs to hold, parquet's {@code
RowRanges.Range} with its object
+ * header and list slot. Scattered survivors make one range each.
Review Comment:
**8. The range charge depends on one shared range list, which no comment
says anymore**
`ROW_RANGE_BYTES` is charged once per range (L715). That is right only
because every column reader of the row group walks the one `RowRanges` list:
`RangeListState` takes `rowRanges.getRanges()`, which parquet returns without
copying. The base said so in three places, and all three are gone:
- here: "One set is held, which every column reader of the row group walks
with a cursor of its own."
- at L558: "rather than left to rebuild them from the store's one row index
per row"
- on `RangeListState`: "the list itself is built once for the whole row
group"
`ParquetReadState.forRead` now also presents null ranges as a neutral
alternative. Passing null at L559, which reads the same rows, or copying the
list in `RangeListState` would give each leaf reader a list of its own, so
memory grows with ranges times leaf columns, against a budget that counts the
ranges once. Could we keep one clause here, e.g. "It is charged once, since
every column reader walks that one list."?
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -477,14 +409,10 @@ private void initializeLateMaterialization() throws
IOException {
return;
}
- // The non-key columns phase 2 reads. If this file has none of them, the
filter saves nothing,
- // since phase 1 already reads every column the file has for every row.
The reader then
- // declines it. The planner already declines a projection of key columns
alone, so this catches
- // a file missing every projected non-key leaf, which also covers the
row-index column. A struct
- // the file has, but none of whose requested fields it has, counts only
under the legacy
- // `returnNullStructIfAllFieldsMissing`, since otherwise the clipped
schema reads one of its
- // other fields to tell a null struct from one whose requested fields are
all null. The checked
- // column is left out too, since a file column of its name is never
decoded.
+ // The non-key columns phase 2 reads. A file with none of them gains
nothing from the filter,
+ // so the reader declines it. A struct none of whose requested fields the
file has counts as
+ // missing only under the legacy `returnNullStructIfAllFieldsMissing`,
since otherwise the
+ // clipped schema reads one of its other fields. The checked column never
counts.
Review Comment:
**11. "The checked column never counts" reads as "never counts as missing"**
Right after "counts as missing only under the legacy
`returnNullStructIfAllFieldsMissing`", it reads as if the checked column were
always present. The code does the opposite: L419 leaves it out of `nonKey`, so
a file column of that name never makes the file look as if it had a non-key
column. The base said "The checked column is left out too, since a file column
of its name is never decoded."
Maybe "The checked column is left out."?
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -57,56 +57,37 @@
/**
* A {@link VectorizedParquetRecordReader} that reads a row group in phases
under a
* {@code ParquetStorageFilter}, so the projected columns of the rows the
filter rejects are never
- * read. The filter is a deterministic conjunct of the post-scan {@code
Filter}, which still holds
- * it, so any phase below may stop short and read the file the way a plain
scan would.
+ * read. The post-scan {@code Filter} still holds the filter, so any phase may
stop short and read
+ * the rows the way a plain scan would.
*
- * <p>Per row group, all three phases driven by the one {@link #fileReader}:
+ * <p>Per row group:
* <ul>
- * <li>phase 0 asks the column index which rows the pushed data filter
allows, metadata only;</li>
- * <li>phase 1 reads the key columns the filter references over those rows,
evaluates the filter
- * per row, and buffers the surviving key values;</li>
- * <li>phase 2 reads the remaining projected columns over the surviving row
ranges.</li>
+ * <li>phase 0 asks the column index which rows the pushed data filter
allows;</li>
+ * <li>phase 1 reads the key columns over those rows, evaluates the filter,
and buffers the
+ * surviving key values;</li>
+ * <li>phase 2 reads the other projected columns over the surviving row
ranges.</li>
* </ul>
+ * The emit path splices the buffered key values into the batch, so the key
columns are read once.
+ * A row group whose buffer passes {@code maxSplicedRowGroupBytes} gives
splicing up, and phase 2
+ * reads its key columns again. If the row ranges alone pass it, the filter is
given up too.
*
- * <p>The emit path then splices the buffered key vectors into the batch, so
phase 2 never reads a
- * key column again. A row group whose buffer has passed
- * {@code spark.sql.parquet.storageFilterPushdown.maxSplicedRowGroupBytes}
gives splicing up, and
- * phase 2 reads its key columns again with the rest. If the survivors' row
ranges alone pass it,
- * the filter is given up for that row group too.
- *
- * <p>A whole file is read without splicing in these cases:
+ * <p>Some files and row groups get less:
* <ul>
- * <li>every key column is missing from it under schema evolution. The
predicate is then constant
- * over the file and is evaluated once, against the constants the reader
materializes for
- * those columns. True keeps the file unfiltered, false skips it;</li>
- * <li>the file has none of the projected non-key columns, or none is
projected. Phase 2 would
- * have nothing to read, so the reader declines the filter and reads the
file plainly;</li>
- * <li>{@code parquet.filter.columnindex.enabled} is false, so its page
index is not used. The
- * filter then only skips row groups it rejects whole.</li>
+ * <li>a file with none of the key columns has a constant predicate,
evaluated once. False skips
+ * the file, and true reads it whole;</li>
Review Comment:
**13. "true reads it whole" vs "read plainly"**
A kept file with none of the key columns leaves `keyColumns` null, so
`loadNextRowGroup` goes to `super.loadNextRowGroup()` and then
`readNextFilteredRowGroup()`, which still skips the pushed data filter's pages
and row groups. The next bullet calls the same path "read plainly", and
`decideAllKeysMissingFile` says "reads it the way a plain scan would", for an
error as well.
Maybe "true reads it plainly"?
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -57,56 +57,37 @@
/**
* A {@link VectorizedParquetRecordReader} that reads a row group in phases
under a
* {@code ParquetStorageFilter}, so the projected columns of the rows the
filter rejects are never
- * read. The filter is a deterministic conjunct of the post-scan {@code
Filter}, which still holds
- * it, so any phase below may stop short and read the file the way a plain
scan would.
+ * read. The post-scan {@code Filter} still holds the filter, so any phase may
stop short and read
Review Comment:
**10. "are never read" is the last description that overstates the skip**
> so the projected columns of the rows the filter rejects are never read.
Phase 1 reads the key columns, which are projected, for every row the pushed
filter kept. Phase 2 reads whole pages, so it also reads rejected rows that
share a page with a survivor. This PR's metrics doc says so: "A row excluded
within a row group may still have been read, as part of a page that held a
survivor." `ParquetStorageFilter`'s class doc now points here instead of
keeping its base text, "skip data-column pages that do not overlap any
surviving row range".
`FileFormat`, `DataSourceScanExec` and `storageFiltersFor` now say "to skip
reading the other columns of the rows they reject". Could this use the same
wording?
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedParquetRecordReader.java:
##########
@@ -185,9 +184,8 @@ public VectorizedParquetRecordReader(boolean useOffHeap,
int capacity) {
/**
* Implementation of RecordReader API. Super's delegates to the overload
below, which runs
- * {@link #initializeInternal()}, so this does not run it a second time.
Kept although it only
- * calls super, because removing it fails MiMa with a {@code
DirectAbstractMethodProblem} on
- * {@code org.apache.hadoop.mapreduce.RecordReader.initialize}.
+ * {@link #initializeInternal()}. Kept although it only calls super, since
removing it fails
+ * MiMa.
Review Comment:
**14. The MiMa reason lost the problem name**
`project/MimaExcludes.scala` excludes `org.apache.spark.sql.execution.*`, so
"removing it fails MiMa" now looks stale to anyone who checks there. It fails
only because MiMa reports a `DirectAbstractMethodProblem` on
`org.apache.hadoop.mapreduce.RecordReader.initialize`, which that exclusion
does not cover. The base named it. Could we keep that?
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -672,42 +578,31 @@ protected boolean suppliesOwnVector(int slot) {
}
/**
- * Counts a file the filter rejects whole, which happens when every key
column is missing from it
- * and the predicate is constant-false for the value the reader would have
materialized. Every row
- * group the pushed data filter left rows in counts as skipped, with every
projected byte of those
- * rows as avoided. Those are all non-key bytes, since the file has no key
column.
+ * Counts a file whose constant predicate is false. Every row group with
rows the pushed data
+ * filter kept counts as skipped, with all their projected bytes avoided,
since the file has no
+ * key column.
*/
private void recordFileSkipped() {
StorageFilterMetrics m = storageFilter.metrics();
List<BlockMetaData> blocks = fileReader.getRowGroups();
for (int blockIdx = 0; blockIdx < blocks.size(); blockIdx++) {
- // Measured against the rows the pushed data filter kept, which is the
baseline every other
- // skip path uses. Resolving them again is a cache hit whenever the two
can differ, because
- // `getFilteredRecordCount()` at initialize resolved every block's
ranges then.
RowRanges blockRanges = pushedFilterRangesFor(blockIdx,
blocks.get(blockIdx).getRowCount());
long survivingRows = blockRanges.rowCount();
if (survivingRows == 0) continue;
- // The key columns are missing from this file, so they contribute
nothing to the walk, and the
- // whole projection is what a plain read would have transferred.
long avoidedBytes = blockAccounting.compressedBytesForRowRanges(
blockIdx, requestedPaths, blockRanges, survivingRows);
m.recordRowGroupSkipped(survivingRows, avoidedBytes);
}
}
/**
- * The rows of a block the pushed data filter allows, at column-index
granularity, or every row of
- * the block when {@link #pageIndexDisabled} says the page index must not be
used.
- *
- * <p>{@code getRowRanges} asks only whether a filter is pushed, not whether
column-index
- * filtering is still enabled, so this is where that conf is honoured. Every
phase reads within
- * these ranges, so narrowing them by an index the read may not trust would
cost rows.
+ * The rows of a block the pushed data filter allows, at column-index
granularity, or every row
+ * when {@link #pageIndexDisabled}. {@code getRowRanges} does not check that
conf itself.
*/
private RowRanges pushedFilterRangesFor(int blockIdx, long blockRowCount) {
if (blockRowCount == 0) {
- // parquet-mr never writes an empty block, and the plain read path skips
one too. For one,
- // `RowRanges.createSingle(0)` below would build Range(0, -1) and trip
its `from <= to`
- // assertion.
+ // parquet-mr never writes an empty block, but
`RowRanges.createSingle(0)` would fail on
Review Comment:
**15. `createSingle(0)` fails only under `-ea`**
The only check in `RowRanges.Range` is `assert from <= to`, so "would fail"
holds only with assertions on, as in tests. In production it builds `Range(0,
-1)` with zero rows, which both callers skip. The base said it would "trip its
`from <= to` assertion". Maybe keep "assertion"?
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetStorageFilter.scala:
##########
@@ -264,39 +217,26 @@ object ParquetStorageFilter {
}
/**
- * Rethrows an error the reader met while applying a storage filter, if it
must not fall back on
- * it by giving the filter up. `fromRead` says the error came from decoding
the key pages rather
- * than from evaluating the filter. The checks run in the executor's own
order:
- * - a fatal error goes out as it is, kill or no kill.
- * - a pending task kill goes out wrapped when the error is one the
executor's kill case takes,
- * see [[throwIfKilled]]. The error may be the kill's interrupt surfacing
through a UDF that
- * wrapped it. `PythonRunner` reads a kill off the task context in the
same way.
- * - a fatal error deeper in the cause chain, found by
`Executor.isFatalError` as deep as the
- * executor would look had a plain plan raised `e`, goes out as itself,
so the executor finds
- * it at the top whatever wraps it on the way.
+ * Rethrows an error the reader met while applying a storage filter, unless
it may give the filter
+ * up on it. `fromRead` says the error came from decoding the key pages. In
the executor's order:
+ * - a fatal error goes out as it is;
+ * - a pending task kill goes out wrapped, see [[throwIfKilled]], since the
error may be the
+ * kill's interrupt surfacing through a UDF;
+ * - a fatal error deeper in the cause chain, as deep as the executor
looks, goes out as itself.
*
- * An `InternalError` among those goes out in an `UnknownError`, a fatal
error of another type.
- * `FileScanRDD` would take a bare `InternalError` from a reader for a
corrupt file under
- * `ignoreCorruptFiles`. A `NonFatal` wrapper does not work either. With a
kill pending, the
- * executor reports one as killed and keeps running. A plain plan raises it
from the post-scan
- * Filter, where neither happens, and the executor stops. It stops on the
`UnknownError` too, at
- * any `spark.executor.killOnFatalError.depth` of 1 or more, since that is
at the top.
+ * An `InternalError` among those goes out in an `UnknownError`, another
fatal error. A bare one
+ * would be taken for a corrupt file under `ignoreCorruptFiles`, and a
`NonFatal` wrapper would
+ * let the executor run on under a pending kill.
*
* An error from decoding differs in three ways:
- * - a plain read raises it from the reader, where `FileScanRDD` wraps a
`NonFatal` one in
- * FAILED_READ_FILE before the executor looks. So a fatal error deeper in
the chain of a
- * `NonFatal` one has to sit one level shallower to count, while any
other error is searched
- * as deep as the executor does. Under `ignoreCorruptFiles` a plain read
may skip the rest of
- * the file on it instead.
+ * - `FileScanRDD` wraps a `NonFatal` one before the executor looks, so a
fatal cause counts one
Review Comment:
**12. "counts one level shallower" can read either way**
> so a fatal cause counts one level shallower;
The code searches one level less deep (`fatalErrorDepth - 1`), because
`FileScanRDD` wraps a plain read's `NonFatal` error in FAILED_READ_FILE, which
puts its cause one level deeper for the executor. "counts one level shallower"
can also read as "counts as if it were one level shallower", which is the
opposite. The base said the cause "has to sit one level shallower to count",
and that a plain read raises it from the reader, which is what the `- 1`
mirrors.
Maybe "so a fatal cause has to sit one level shallower to count;"?
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -523,75 +450,61 @@ private void initializeLateMaterialization() throws
IOException {
}
/**
- * Loads the next row group with rows left to emit, through the three phases
of the class javadoc.
- * Phase 2 reads the non-key columns over the survivors while splicing, the
whole projection over
- * the survivors once splicing is given up, and the whole projection over
- * {@code pushedFilterRanges} once the filter is. Returns false when the row
groups run out, which
- * ends a read the filter narrowed.
+ * Loads the next row group with rows left to emit, through the phases of
the class javadoc.
+ * Returns false when the row groups run out.
*/
@Override
protected boolean loadNextRowGroup() throws IOException {
if (keyColumns == null) return super.loadNextRowGroup();
- // The previous row group is fully emitted by the time this is called, so
its pages are done
- // with. Released here rather than at the next assignment, so a row group
the filter empties,
- // or the end of the file, does not keep the previous one's pages alive.
Its column readers go
- // too. They hold the row ranges it was read over, which the budget no
longer counts, and the
- // decoders of their last page.
+ // The previous row group is fully emitted, so its pages and column
readers go now rather than
+ // at the next assignment, which a row group the filter empties would
delay.
Review Comment:
**9. Why the readers go before the next row group's phase 1**
The base also said: "Its column readers go too. They hold the row ranges it
was read over, which the budget no longer counts". That is the reason for every
row group, not only for the one the filter empties. The next assignment
(`dataPages` at L524, `installRowGroup` at L559) always comes after the next
row group's phase 1, whose budget starts from zero (L667-668).
Folding the release into that assignment, or dropping
`releaseRowGroupReaders()` because `installRowGroup` replaces the readers
anyway, would keep the previous row group's ranges and pages alive through
every phase 1. That is up to about twice the cap outside the memory manager.
This is about the call site, not the method javadoc from round-1 comment 8.
Could we keep "The readers hold the row ranges it was read over, which the
next row group's budget does not count."?
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -190,11 +151,10 @@ public class LateMaterializationParquetRecordReader
extends VectorizedParquetRec
/**
* Whether {@code parquet.filter.columnindex.enabled} is false, so this
file's page index must not
- * be used. That turns off phase 0 and every partial read. A wrong index
would pair a row's key
- * with another row's values, or return rows the filter rejects once
splicing is given up. The
- * post-scan Filter catches neither, since it takes the reader's mark on
those rows. Parquet's
- * partial read does not check the conf itself. Read off {@link
#readOptions}, so it agrees with
- * the row count {@link #fileReader} reported.
+ * be used. That turns off phase 0 and every partial read, which parquet
does not check itself.
+ * It is the way out of a wrong page index, which would pair a row's key
with another row's
+ * values, and which the post-scan Filter cannot catch on the rows this
reader marks checked.
+ * Read off {@link #readOptions}, so it agrees with {@link #fileReader}.
Review Comment:
**7. `pageIndexDisabled` no longer says what it has to agree with**
The base said "so it agrees with the row count `{@link #fileReader}`
reported". That is the invariant. `totalRowCount` is
`fileReader.getFilteredRecordCount()`, computed with the same
`useColumnIndexFilter`, and `VectorizedParquetRecordReader.nextBatch` stops
once `rowsReturned >= totalRowCount`, before it asks `loadNextRowGroup`. If
another switch were ORed in here, this reader would read whole row groups that
parquet counted narrower, and `nextBatch` would end the split early, dropping
its last rows, matching ones included, with no error. "agrees with fileReader"
no longer says which part has to agree. The base class's `loadNextRowGroup`
javadoc dropped the same point ("totalRowCount already says when they run out").
Also, the reason restored above in round 1 covers the mispaired values, but
no longer the base's "or return rows the filter rejects once splicing is given
up", which is the case of a narrowed row group whose rows are still marked
checked.
Could we keep "the row count `{@link #fileReader}` reported"?
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetBlockAccounting.java:
##########
@@ -84,29 +77,14 @@ boolean hasOffsetIndexes(int blockIndex, Set<ColumnPath>
paths) {
}
/**
- * Compressed bytes a reader transfers for the leaf columns named by {@code
paths} when it reads
- * exactly {@code rowRanges} of the given block. Page headers and the
dictionary page are
- * included, since both are read whenever any page of a chunk is read.
{@code rowRangeCount} is
- * {@code rowRanges.rowCount()}, passed in because that walks every range
and the caller has it.
- *
- * <p>Two sources, chosen so this never causes IO of its own:
- * <ul>
- * <li>{@code rowRanges} covers the whole block, and the answer is the sum
of the chunks'
- * {@code getTotalSize()}, which is already in the footer. This is the
case that matters.
- * Whenever nothing else has built the block's {@link
ColumnIndexStore}, {@code rowRanges}
- * is necessarily the whole block, because a narrower range can only
come from column-index
- * filtering, which builds the store as a side effect.
- * <li>{@code rowRanges} is a strict subset, and the walk goes over the
offset index, as
- * parquet's own read path does, adding the dictionary page the way
- * {@code calculateOffsetRanges} does. The store is guaranteed to
exist here, so the walk is
- * pure metadata arithmetic. For ranges a reader narrowed itself,
which column-index
- * filtering had no hand in, that guarantee is an ordering one, since
the reader's own read
- * of those ranges built the store first. A new caller has to keep
that order.
- * </ul>
+ * Compressed bytes a reader transfers for the columns in {@code paths} when
it reads exactly
+ * {@code rowRanges} of the block, page headers and the dictionary page
included.
+ * {@code rowRangeCount} is {@code rowRanges.rowCount()}, which the caller
has.
*
- * <p>Columns absent from this physical file (schema evolution) contribute
nothing, which is
- * correct, since the reader transfers nothing for them. They fall out of
the walk by themselves,
- * which goes over the block's chunks rather than over {@code paths}.
+ * <p>A whole block is the chunks' sizes from the footer. A strict subset is
walked over the
+ * offset index, as parquet's read does. That reads the block's {@link
ColumnIndexStore}, which
+ * costs no IO only once a read of those ranges has built it, so a new
caller has to keep that
Review Comment:
**5. The no-IO note lost the cached-store hazard and the column-index case**
The no-IO precondition moved here from the class javadoc, as HyukjinKwon
checked. Two parts of the deleted text did not come with it:
1. The old text said a walk before the store exists "would read the offset
indexes itself and cache a store over the reader's current columns". The cached
store is what breaks things. `getColumnIndexStore` builds it over the current
paths and keeps it for the block, so a later `readFilteredRowGroup` over other
columns gets a null offset index and throws an NPE in parquet's
`filterOffsetIndex`, which the `MissingOffsetIndexException` catch does not
take. With no data filter pushed, nothing builds the store before phase 2, so a
walk over `survivors` placed after phase 1 would break phase 2's read. "costs
no IO" makes it read as a cost only.
2. "only once a read of those ranges has built it" leaves out column-index
filtering, which `recordFileSkipped` and `nonKeyBaselineBytes` rely on:
`getFilteredRecordCount()` at initialize built every block's store over the
whole projection.
The class javadoc's "with no IO of its own" (L41) is now unconditional too.
Could this say, e.g., "That needs the block's store, which column-index
filtering or a read of those ranges over the same columns builds. A walk before
that builds and keeps one over the reader's current columns, so a later partial
read of other columns fails."?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]