peter-toth commented on code in PR #59269:
URL: https://github.com/apache/spark/pull/59269#discussion_r4211298887
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/FileSourceStrategy.scala:
##########
@@ -418,11 +441,19 @@ object FileSourceStrategy extends Strategy with
PredicateHelper with Logging {
Alias(KnownNotNull(CreateStruct(structColumns.toImmutableArraySeq)),
FileFormat.METADATA_NAME)(exprId = metadataStruct.exprId)
execution.ProjectExec(
- readDataColumns ++ partitionColumns :+ metadataAlias, scan)
+ readDataColumns ++ storageFilterCheckedColumn ++ partitionColumns :+
metadataAlias, scan)
}.getOrElse(scan)
+ // The offered conjuncts are skipped on the rows the reader marks as
checked. They are
+ // matched by `ExpressionSet`, since `storageFilters` carry the
relation's column names.
+ val postScanFilters = storageFilterCheckedColumn match {
+ case Some(checked) =>
+ val offered = ExpressionSet(storageFilters)
+ afterScanFilters.toSeq.map(f => if (offered.contains(f)) Or(checked,
f) else f)
Review Comment:
Thanks, fixed in
[9a71742c400](https://github.com/apache/spark/pull/59269/commits/9a71742c400a9104bb4595ac582a449a6e6fede4).
The rewrite is `IF(checked, true, conjunct)` now, as @dongjoon-hyun suggested
on the same line. A null mark then leaves the conjunct, and the `AND` around
it, as they were.
Since the fix for his comment on L373, the column goes only to a scan that
returns columnar batches into whole-stage codegen, which parquet-mr does not
read. So the new test checks the expression itself, generated and interpreted:
a null mark, a bloom that rejects the row, and a conjunct after it that throws.
It also checks that the `OR` form throws.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -1982,13 +1982,16 @@ object SQLConf {
"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. A row group where
it stops reads " +
+ "more than it saves, and the answer does not change. The post-scan
filter skips it on " +
+ "the rows the reader applied it to, except where one more column would
push the scan " +
+ "past spark.sql.codegen.maxFields. A row group where it stops reads " +
Review Comment:
Done in
[9a71742c400](https://github.com/apache/spark/pull/59269/commits/9a71742c400a9104bb4595ac582a449a6e6fede4).
The conf doc and the user-facing section now say that a table with a column
named `_tmp_storage_filter_checked` is not offered storage filters.
A file that holds such a column outside the relation's schema is handled now
too. The late-materialization reader never reads it and writes the marks itself
(@dongjoon-hyun's comment on L203). parquet-mr does not get the column, since
it goes only to a scan that returns columnar batches.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/FileSourceStrategy.scala:
##########
@@ -194,6 +200,10 @@ object FileSourceStrategy extends Strategy with
PredicateHelper with Logging {
outputDataSchema: StructType): Seq[Expression] = {
val sparkSession = fsRelation.sparkSession
if (!fsRelation.fileFormat.supportsStorageFilterPushdown(sparkSession))
return Nil
+ if (fsRelation.schema.fieldNames.exists(
Review Comment:
Thanks, done in
[9a71742c400](https://github.com/apache/spark/pull/59269/commits/9a71742c400a9104bb4595ac582a449a6e6fede4).
`suppliesOwnVector` answers true for the checked slot, and its column is left
out of `nonKey`, compared case-insensitively. So the file's column is never
decoded.
A new test reads a file with an upper-case INT64 column of that name, which
failed before. It also reads a file whose only other column is a boolean one of
that name, which must still be declined.
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/LateMaterializationParquetRecordReader.java:
##########
@@ -595,6 +634,8 @@ protected boolean loadNextRowGroup() throws IOException {
m.rowsExcludedWithinRowGroup().add(baselineRows - finalRowCount);
}
+ // The rows phase 2 read are exactly the survivors unless the filter was
given up.
+ rowsChecked = !filterGivenUp;
Review Comment:
Thanks for the detailed analysis. I'd keep marking these row groups checked,
and keep the caveat documented.
- Splicing is given up mostly because the survivors pass the budget. So
these are the row groups with the most survivors, and re-checking them would
cost the most.
- A wrong offset index can already pair a row's key with another row's
values, which the `Filter` cannot catch either.
- `parquet.filter.columnindex.enabled=false` turns both off, and the conf
doc points to it.
I fixed the two places you quoted. `FileFormat`'s contract now says
"evaluated on the row", without "the row's own values". `FileSourceStrategy` no
longer calls the check exact.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/FileSourceStrategy.scala:
##########
@@ -350,23 +360,36 @@ object FileSourceStrategy extends Strategy with
PredicateHelper with Logging {
}
}
- val outputDataSchema = (readDataColumns ++
generatedMetadataColumns).toStructType
-
// Offered conjuncts become `storageFilters` on the scan and stay in the
post-scan Filter too.
- // This runs here because eligibility depends on `outputDataSchema`.
- val storageFilters =
- storageFiltersFor(normalizedFilters, fsRelation, readDataColumns,
outputDataSchema)
+ // This runs here because eligibility depends on the data columns the
reader reads.
+ val storageFilters = storageFiltersFor(normalizedFilters, fsRelation,
readDataColumns,
+ (readDataColumns ++ generatedMetadataColumns).toStructType)
+ // The column the reader marks the rows it checked in, which the
post-scan Filter reads. See
+ // `FileFormat.buildReaderWithStorageFilters`. It is left out where it
alone would push the
+ // scan past the whole-stage codegen field limit, which would cost the
scan its columnar
+ // output. The post-scan Filter then evaluates the offered conjuncts on
every row.
+ val storageFilterCheckedColumn = Option.when(storageFilters.nonEmpty) {
+ AttributeReference(FileFormat.STORAGE_FILTER_CHECKED_COLUMN_NAME,
BooleanType)()
+ }.filterNot { checked =>
Review Comment:
Thanks, done in
[9a71742c400](https://github.com/apache/spark/pull/59269/commits/9a71742c400a9104bb4595ac582a449a6e6fede4),
with your condition. The column goes only to a scan that returns columnar
batches into whole-stage codegen with it, where the `Filter` and the `Project`
are fused. Everywhere else the scan still gets the filter but no column, and
the `Filter` evaluates the bloom on every row, as before this PR.
The test now has a partition column, and covers the limit with and without
the column, and whole-stage codegen off. It checks `supportsColumnar` at each.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/FileSourceStrategy.scala:
##########
@@ -350,23 +360,36 @@ object FileSourceStrategy extends Strategy with
PredicateHelper with Logging {
}
}
- val outputDataSchema = (readDataColumns ++
generatedMetadataColumns).toStructType
-
// Offered conjuncts become `storageFilters` on the scan and stay in the
post-scan Filter too.
- // This runs here because eligibility depends on `outputDataSchema`.
- val storageFilters =
- storageFiltersFor(normalizedFilters, fsRelation, readDataColumns,
outputDataSchema)
+ // This runs here because eligibility depends on the data columns the
reader reads.
+ val storageFilters = storageFiltersFor(normalizedFilters, fsRelation,
readDataColumns,
+ (readDataColumns ++ generatedMetadataColumns).toStructType)
+ // The column the reader marks the rows it checked in, which the
post-scan Filter reads. See
+ // `FileFormat.buildReaderWithStorageFilters`. It is left out where it
alone would push the
+ // scan past the whole-stage codegen field limit, which would cost the
scan its columnar
+ // output. The post-scan Filter then evaluates the offered conjuncts on
every row.
+ val storageFilterCheckedColumn = Option.when(storageFilters.nonEmpty) {
Review Comment:
Thanks. I'd keep the column there. Whether a file has an offset index is
known only when the reader opens it, and one scan can mix both kinds of files.
The cost is a load and a branch per row, next to the bloom's hash and probes.
The `columnindex` conf is known at planning, but it is an escape hatch, and
I'd rather not add a planner rule for it. The conf doc now says that in both
cases the post-scan filter evaluates the filter on each row of a row group with
a survivor.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/FileSourceStrategy.scala:
##########
@@ -418,11 +441,19 @@ object FileSourceStrategy extends Strategy with
PredicateHelper with Logging {
Alias(KnownNotNull(CreateStruct(structColumns.toImmutableArraySeq)),
FileFormat.METADATA_NAME)(exprId = metadataStruct.exprId)
execution.ProjectExec(
- readDataColumns ++ partitionColumns :+ metadataAlias, scan)
+ readDataColumns ++ storageFilterCheckedColumn ++ partitionColumns :+
metadataAlias, scan)
}.getOrElse(scan)
+ // The offered conjuncts are skipped on the rows the reader marks as
checked. They are
+ // matched by `ExpressionSet`, since `storageFilters` carry the
relation's column names.
+ val postScanFilters = storageFilterCheckedColumn match {
+ case Some(checked) =>
+ val offered = ExpressionSet(storageFilters)
+ afterScanFilters.toSeq.map(f => if (offered.contains(f)) Or(checked,
f) else f)
Review Comment:
Thanks, done in
[9a71742c400](https://github.com/apache/spark/pull/59269/commits/9a71742c400a9104bb4595ac582a449a6e6fede4),
together with @sunchao's comment on this line. It is `IF(checked, true,
conjunct)` now.
--
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]