peter-toth commented on code in PR #58895:
URL: https://github.com/apache/spark/pull/58895#discussion_r4084294794


##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedParquetRecordReader.java:
##########
@@ -421,11 +552,313 @@ public boolean nextBatch() throws IOException {
     return true;
   }
 
+  /**
+   * Splicing emit path. Closes the previous emit's dequeued key vectors 
(releasing survivor memory
+   * incrementally), advances to the next row group if needed via {@link 
#checkEndOfRowGroup()},
+   * dequeues one survivor key vector per key column, drives non-key column 
readers for {@code num}
+   * rows, and assembles a fresh {@link ColumnarBatch} interleaving key 
(dequeued) and non-key
+   * (persistent value-vector) slots in the original projection order.
+   * The per-emit batch is a transient view over vectors owned elsewhere; see 
{@link #close()} and
+   * {@link #closeSplicingState()}.
+   */
+  private boolean nextBatchSplicing() throws IOException {

Review Comment:
   Fair question, and it is now answered in the description under **Design 
decisions** as well as in the reader's own note.
   
   Re-reading is where this started, and it was measured slower: on TPCDS q37, 
3949 ms splicing against 4511 ms re-reading, with the feature off at 3199 ms. 
The 562 ms between the two is one more read of `cs_item_sk`, and nothing 
absorbs it on object storage -- parquet caches footers rather than pages, and 
S3A's default input stream caches nothing, so it is a new GET (the caching 
stream types are opt-in).
   
   Two corrections to the cost model. The re-read does not remove the `LIMIT` 
stall, since phase 1 still walks the whole row group to build `finalRanges`. 
And it would not remove the buffering either, for the same reason -- what it 
removes is the splicing machinery, which is real but is the part with tests 
around it.
   
   Your comment did change the planner, though. q37 is an all-keys projection, 
and on that shape neither design can win: the reader has to read the key column 
to evaluate the filter on it, so the scan reads the same columns for the same 
rows as a plain one. That is what 3199 against 3949 shows. Extraction is now 
dropped when every projected data column is a key column of the filter.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -1885,6 +1885,21 @@ object SQLConf {
     .booleanConf
     .createWithDefault(true)
 
+  val PARQUET_STORAGE_FILTER_PUSHDOWN_ENABLED =
+    buildConf("spark.sql.parquet.storageFilterPushdown.enabled")
+      .doc("If true, allows the vectorized Parquet reader to evaluate runtime 
storage filters " +
+        "(e.g. bloom filters from join runtime filtering) at the scan level 
using late " +
+        "materialization: read key columns first, evaluate the filter per row, 
then read data " +
+        "columns restricted to surviving rows. This is a planning-time 
decision only: when " +
+        "false, no storage filter is attached to a scan in the first place and 
the filter is " +
+        "applied as an ordinary post-scan filter instead. Note that the 
surviving key values of " +
+        "a whole row group are buffered before the first batch of that row 
group is produced, " +
+        "so a task holds up to one extra copy of the key columns for one row 
group.")
+      .version("5.0.0")

Review Comment:
   Master-only on purpose, so `5.0.0` is right. The reader needs the parquet 
1.18 APIs -- `RowRanges` public in `filter2.columnindex`, 
`RowRanges.builder()`, `addSelectedRow(long)` and a public 
`ParquetFileReader.getRowRanges(int)` -- and that upgrade (#58120) went to 
master alone, with `branch-4.x` still on 1.17.0.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/FileSourceStrategy.scala:
##########
@@ -151,6 +152,63 @@ object FileSourceStrategy extends Strategy with 
PredicateHelper with Logging {
     }
   }
 
+  /**
+   * Splits `afterScanFilters` into bloom-filter conjuncts that can be pushed 
to the storage layer
+   * for late materialization (returned as the first element) and the 
remaining filters that stay as
+   * a post-scan FilterExec (returned as the second element).
+   *
+   * Eligibility (statically checked here so the runtime never silently loses 
the filter):
+   *  - The storage-filter pushdown SQL conf is on.
+   *  - The file format is exactly [[ParquetFileFormat]]. Subclasses are 
excluded on purpose: they
+   *    may customize reading by overriding `buildReaderWithPartitionValues`, 
and attaching storage
+   *    filters would route the scan through `ParquetFileFormat`'s own reader 
instead, silently
+   *    dropping whatever the subclass does.
+   *  - The vectorized reader is feasible for the schema the reader will 
actually see, i.e.
+   *    `partitionSchema ++ outputDataSchema` -- the same schema 
`ParquetFileFormat.buildReader`
+   *    derives `enableVectorizedReader` from.
+   *  - The conjunct is a top-level [[BloomFilterMightContain]] (not nested 
under OR/NOT).
+   *  - The conjunct is deterministic. `ParquetStorageFilter.test` evaluates 
the predicate without
+   *    calling `BasePredicate.initialize(partitionIndex)`, which 
`GeneratePredicate` emits for a
+   *    `Nondeterministic` expression, so a non-deterministic conjunct would 
fail at task time. No
+   *    such bloom exists today, because the only producer is 
`InjectRuntimeFilter` and a join key
+   *    is deterministic, but this gate should not depend on a distant rule.
+   *  - The bloom's value-side references are projected data columns whose 
type the reader's value
+   *    copier supports (see [[ParquetStorageFilter.isSupportedKeyType]]).
+   *
+   * If any condition fails, ALL bloom filters stay in the second element to 
preserve the existing
+   * fallback behavior.
+   */
+  private def extractStorageFilters(
+      afterScanFilters: ExpressionSet,
+      fsRelation: HadoopFsRelation,
+      readDataColumns: Seq[Attribute],
+      outputDataSchema: StructType): (Seq[Expression], ExpressionSet) = {
+    val sparkSession = fsRelation.sparkSession
+    val sqlConf = sparkSession.sessionState.conf
+    if (!sqlConf.parquetStorageFilterPushdownEnabled) return (Nil, 
afterScanFilters)
+    if (fsRelation.fileFormat.getClass != classOf[ParquetFileFormat]) {

Review Comment:
   Done, as `FileFormat.supportsStorageFilter(expr)` defaulting to false. 
`FileSourceStrategy` no longer imports the parquet package, and the key-type 
list lives next to the copier that defines it. The subclass exclusion moved 
into `ParquetFileFormat`'s override, where it reads as a fact about that 
reader, exactly as you put it.
   
   One deviation: the signature takes only the expression, not the read schema. 
Everything the format needs is reachable from the expression's references, and 
schema-level feasibility is `supportBatch`, which the planner asks a few lines 
above. It can grow the parameter when a second format needs one.
   



-- 
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]

Reply via email to