dongjoon-hyun commented on code in PR #59324:
URL: https://github.com/apache/spark/pull/59324#discussion_r4247335315
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -1976,37 +1969,19 @@ 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. 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. Without a " +
Review Comment:
**1. conf doc drops the side effect of `parquet.filter.columnindex.enabled`**
The doc now offers `parquet.filter.columnindex.enabled=false` as the way to
stop relying on the page index, but the previous text also said what else it
turns off:
> A read with pushed data filters already relies on the page index, and that
conf turns it off there too.
That still holds. The conf is parquet's own
(`readOptions.useColumnIndexFilter()`), so setting it for the session also
disables column-index filtering for every ordinary pushed data filter on every
Parquet scan, not only for this feature. A user who follows this doc to work
around one bad file can slow down unrelated queries without knowing why.
Could we keep one clause, e.g. "..., which also turns off page skipping for
pushed data filters"?
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetStorageFilter.scala:
##########
@@ -30,30 +30,22 @@ import org.apache.spark.util.Utils
/**
* The SQL metrics the reader updates while applying a
[[ParquetStorageFilter]], created by
- * `ParquetFileFormat.storageFilterMetrics`. Every counter is scoped to what
the storage filter
- * saved against a read of the same projection without one. The one cost taken
off is a second
- * key read in a row group that gave splicing up, below. A row group whose
filter was given up
- * reads its key columns twice and counts nothing, so the counters can show a
saving on a scan
- * that read more than it would have with the feature off. Nor do they take
off the offset indexes
- * phase 2 reads for a narrowed row group when no data filter is pushed, which
a plain read does not
- * read. Bytes are the compressed bytes the reader did not ask parquet for.
The file system can
- * still fetch some of them, for example when a vectored read merges nearby
ranges.
+ * `ParquetFileFormat.storageFilterMetrics`. They count only what the filter
saved against a read of
Review Comment:
**2. metrics doc now claims a net saving it does not measure**
"They count only what the filter saved against a read of the same projection
without one" reads as a net saving, but the caveat that qualified it was
dropped:
> Nor do they take off the offset indexes phase 2 reads for a narrowed row
group when no data filter is pushed, which a plain read does not read.
With no pushed data filter, a plain read never touches the offset indexes,
while phase 2's narrowed `readFilteredRowGroup` does, and the counters don't
take that off. So `bytes avoided by storage filter (page filtering)` overstates
the saving on exactly the scans this feature targets. Since the metric is
user-visible in the SQL UI, I'd keep that sentence (it's one line), or at least
not say "only".
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -601,25 +514,18 @@ protected boolean loadNextRowGroup() throws IOException {
}
fileReader.setRequestedSchema(spliceCurrentRowGroup ? nonKeyColumns :
requestedColumns);
- // The footer check above catches every file written without offset
indexes. This catch is
- // the backstop for an offset index the footer lists but parquet fails
to read, which empties
- // that block's store, and only that block's, since parquet keeps one
per block. The row group
- // is then read again over `pushedFilterRanges`, which that empty store
has already widened to
- // the whole block, so the retry cannot throw. The next row group is
narrowed as usual.
- //
- // `MissingOffsetIndexException` is parquet's internal API, as are the
`ColumnIndexStore` and
- // `OffsetIndex` that `ParquetBlockAccounting` reads. Revisit both on a
parquet upgrade. If
- // parquet stops throwing this one as it is, the damaged-index test in
- // `ParquetStorageFilterSuite` fails.
+ // An offset index the footer lists but parquet fails to read empties
that block's store.
+ // The row group is then read again over `pushedFilterRanges`, which the
empty store has
+ // widened to the whole block, so the retry cannot throw.
`MissingOffsetIndexException` is
+ // parquet's internal API, and the damaged-index test fails if parquet
stops throwing it.
Review Comment:
**5. parquet-upgrade note no longer covers `ColumnIndexStore` /
`OffsetIndex`**
The previous comment was the one place that said `ColumnIndexStore` and
`OffsetIndex`, which `ParquetBlockAccounting` imports from
`org.apache.parquet.internal.*`, are also parquet internal API, and to revisit
them on a parquet upgrade. Now only `MissingOffsetIndexException` is mentioned,
and `ParquetBlockAccounting` has no such note of its own.
The damaged-index test pins only the exception. Nothing would flag a change
to the store/offset-index API that silently miscounts the byte metrics. Could
we keep "as are the `ColumnIndexStore` and `OffsetIndex` that
`ParquetBlockAccounting` reads", here or in `ParquetBlockAccounting`'s class
javadoc?
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/DataSourceScanExec.scala:
##########
@@ -303,10 +303,9 @@ trait FileSourceScanLike extends DataSourceScanExec with
SessionStateHelper {
// Filters on non-partition columns.
def dataFilters: Seq[Expression]
- // Filters the storage layer evaluates to prune value-column IO based on
key-column evaluation.
- // These may reference subqueries (e.g. a runtime bloom filter built from a
join build side),
- // which are materialized on the driver while the RDD is built, before the
reader is serialized.
- // Nil for a scan that does not support pushing them.
+ // Filters the storage layer evaluates to skip reading the columns they do
not reference, see
Review Comment:
**3. storage filters do not skip the unreferenced columns, only their
rejected rows**
"to skip reading the columns they do not reference" isn't accurate: phase 2
always reads those columns for the surviving rows. Only the rejected rows'
pages are skipped. The same wording is in
`FileSourceStrategy.storageFiltersFor` (line 155).
`FileFormat.buildReaderWithStorageFilters` in this PR says it correctly: "to
skip reading the other columns of the rows they reject".
Suggest using that wording in both places.
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedParquetRecordReader.java:
##########
@@ -523,11 +517,9 @@ protected final void installRowGroup(PageReadStore pages,
RowRanges rowRanges)
}
/**
- * Drops the column readers of the row group installed last, and the
row-index iterator, both of
- * which hold the row ranges that row group was read over. For a subclass
that does work of its
- * own between two row groups, so that work does not run with the previous
ranges still held.
- * Call it only from {@link #loadNextRowGroup}, once the row group is fully
emitted. A batch read
- * after it needs another row group installed first.
+ * Drops the column readers and the row-index iterator of the row group
installed last, for a
Review Comment:
**8. `releaseRowGroupReaders` lost its post-call precondition**
This protected method lost its post-call contract:
> A batch read after it needs another row group installed first.
After this call every leaf's column reader is null, so a subclass that calls
`nextBatch()`/`readBatch` before `installRowGroup` fails with an NPE. "Call it
only from `loadNextRowGroup`" says when to call it, not what must happen
afterwards. I'd keep that one sentence, since this is an extension point.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/FileSourceStrategy.scala:
##########
@@ -443,10 +428,9 @@ object FileSourceStrategy extends Strategy with
PredicateHelper with Logging {
}.getOrElse(scan)
// The offered conjuncts are skipped on the rows the reader marks as
checked. An `If` rather
- // than an `Or`, so that a null mark leaves a conjunct as it was, an
`And` around it
- // included, and so that subexpression elimination cannot hoist what the
conjunct shares
- // with another one ahead of the skip. They are matched by
`ExpressionSet`, since
- // `storageFilters` carry the relation's column names.
+ // than an `Or`, so a null mark leaves the conjunct as it was, an `And`
around it included,
+ // and subexpression elimination cannot hoist a shared hash ahead of the
skip. They are
Review Comment:
**10. format-agnostic comment now names a bloom-specific hash**
Nit: "cannot hoist a shared hash ahead of the skip" ties this
format-agnostic planner code to the bloom implementation. The previous "what
the conjunct shares with another one" holds for any storage filter. Since the
`If` guards against hoisting any shared subexpression, I'd keep the general
wording so a future non-bloom storage filter doesn't read this as bloom-only.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetStorageFilter.scala:
##########
@@ -125,33 +106,21 @@ class ParquetStorageFilter private (
val maxSplicedRowGroupBytes: Long) extends Serializable {
/**
- * The predicate this filter evaluates, which the reader resolves once and
then runs per row. It
- * is handed out rather than wrapped, because resolving the `lazy val` costs
a volatile read that
- * a per-row loop would otherwise pay. Codegen-produced predicates can be
awkward to serialize
- * from driver to executor, so it is built on first use on the executor.
+ * The predicate this filter evaluates, built on first use on the executor,
since a generated one
Review Comment:
**9. `preparedPredicate` doc dropped why it is handed out, not wrapped**
The doc dropped why this is handed out rather than wrapped: reaching it
through the `lazy val` is a volatile read, so a per-row caller must resolve it
once. The reason now lives only at the one call site in `evaluateStorageFilter`
("Out of the loop, since resolving it is a volatile read").
That leaves nothing on this class to stop a convenience `eval(row)` wrapper,
or a new caller reading it per row. One clause here would keep the contract
with the member.
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -854,25 +717,20 @@ private RowRanges evaluateStorageFilter(
if (currentKeyAccumulators == null) allocateKeyAccumulators();
splicedBytes += appendSurvivorRowToAccumulators(r);
} catch (RuntimeException e) {
- // The buffer is this reader's own and optional, so failing to
fill it gives splicing up
- // rather than the filter, and the memory goes back at once. What
fails here:
- // - a survivor vector that cannot grow;
- // - a key value the predicate did not read on this row, such as
`b` in
- // `coalesce(a, b)`, since the copy decodes every key;
- // - a bug of ours, which only the WARN then shows.
- // Phase 2 reads the key columns the way a plain read does, so a
value that cannot be
- // decoded fails the read only where a plain read fails.
+ // The buffer is optional, so failing to fill it gives splicing
up, not the filter. That
Review Comment:
**7. splicing catch dropped why absorbing a decode error is safe**
Thanks for restoring "a bug of ours" here. The other dropped sentence is the
one that makes absorbing a decode error safe:
> Phase 2 reads the key columns the way a plain read does, so a value that
cannot be decoded fails the read only where a plain read fails.
The catch swallows a `RuntimeException` from decoding a key the predicate
did not read (`b` in `coalesce(a, b)`). That is correct only because of this
property of phase 2, and the comment should name what it depends on.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/DataSourceScanExec.scala:
##########
@@ -814,16 +807,11 @@ case class FileSourceScanExec(
readRDD
}
- // Materialize scalar subqueries inside storage filters to literals and bind
AttributeReferences
- // to BoundReferences targeting positions in `requiredSchema`. Subqueries
must have been prepared
- // by SparkPlan before this is forced (same contract as `pushedDownFilters`).
- //
- // No conf check here, since the conf decides at planning time whether a
scan is offered storage
- // filters at all, and re-reading it now could only make this scan drop work
it already has.
- //
+ // The storage filters with their scalar subqueries materialized, bound to
`requiredSchema`.
Review Comment:
**6. dropped why `preparedStorageFilters` does not re-check the conf**
This dropped a design invariant rather than narrative:
> No conf check here, since the conf decides at planning time whether a scan
is offered storage filters at all, and re-reading it now could only make this
scan drop work it already has.
The public doc also lost "This is a planning-time decision". Without either,
a missing conf check here looks like an oversight, and someone may add one.
That would make the executed scan differ from the plan, whose explain output
still shows `StorageFilters`. I'd keep the sentence.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -1976,37 +1969,19 @@ 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. The filter
also stays in the " +
+ "post-scan filter, so the answer does not change, with two exceptions.
Under " +
Review Comment:
**4. 'with two exceptions', followed by an unsupported case**
"so the answer does not change, with two exceptions" is followed a few
sentences later by "a file holding one outside the table's schema is not
supported". A reader can't tell whether that is a third way for the answer to
change, or a failure.
For example, if the plain reader is used for such a file (say
spark.sql.parquet.enableVectorizedReader is flipped after planning), it decodes
the file's `_tmp_storage_filter_checked` values and the post-scan Filter trusts
them. Could the doc say what "not supported" means for the user (an error, or
rows not filtered), or drop the count ("with these exceptions")?
--
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]