sunchao opened a new pull request, #57443:
URL: https://github.com/apache/spark/pull/57443

   ### Why are the changes needed?
   
   Spark's runtime Bloom-filter rule normally recognizes a small join input by 
finding a selective predicate in its logical plan. Persisting that input 
replaces the predicate with an `InMemoryRelation`, even after the cache has 
been fully materialized. The selected keys are still available cheaply, but 
Spark no longer recognizes them as a useful filter source and may shuffle the 
entire other side of the join.
   
   For example, these two joins represent the same selected keys:
   
   ```scala
   val selected = requests.filter($"tenant_id" === 42).select("request_id")
   
   events.join(selected, "request_id")
   
   val cached = selected.persist(StorageLevel.MEMORY_AND_DISK)
   cached.count()
   events.join(cached, "request_id")
   ```
   
   The uncached join can receive a runtime Bloom filter. The cached join 
currently cannot because the optimizer sees the cache rather than the selective 
predicate that produced it. The result is especially wasteful when the event 
side is large and most of its keys do not appear in the already-materialized 
cache.
   
   There is also no useful default interval for a cached side that is too large 
to broadcast: both `spark.sql.autoBroadcastJoinThreshold` and 
`spark.sql.optimizer.runtime.bloomFilter.creationSideThreshold` default to 10 
MB. A 40 MB cached key set can therefore force a shuffle while being rejected 
as a Bloom-filter input, even though scanning the materialized cache does not 
recompute its original query.
   
   [SPARK-58272](https://issues.apache.org/jira/browse/SPARK-58272) addresses 
the missed optimization. It revisits the unmerged cache-awareness proposed in 
[#39377](https://github.com/apache/spark/pull/39377), while adding explicit 
safeguards for cache materialization, replayability, and filtering benefit.
   
   ### What changes were proposed in this PR?
   
   This change lets the existing runtime Bloom-filter rule recognize a fully 
materialized, disk-backed cached relation as a safe source of join keys. The 
optimizer reads the cache's exact row count and byte size and builds the Bloom 
filter from cached output, while retaining its existing join-shape, 
key-lineage, application-size, and filter-count checks. The resulting join 
remains an ordinary Spark join; applications do not need a specialized helper 
or a rewritten query.
   
   A cache is eligible only when every partition of its current generation has 
completed and its logical and physical inputs can be replayed without changing 
their rows. Cache bookkeeping is generation-specific and publishes its loaded 
state and partition statistics atomically; incomplete iterator consumption and 
late completions from an earlier cache generation cannot make a rebuilt cache 
appear complete. File-backed inputs are restricted to trusted built-in readers, 
and their actual instantiated file-scan RDDs must be strict about missing and 
corrupt files. Memory-only caches, partially materialized caches, opaque user 
functions, nondeterministic or runtime-replaceable expressions, and best-effort 
or unknown readers are excluded.
   
   Materialization alone is not treated as proof that a Bloom filter will help. 
The optimizer requires either a selective predicate in the cache's original 
plan or application-side join-key statistics showing that the cache's exact row 
count is smaller than the application's distinct key count. Statistics are 
traced back through aliases and projections to the originating scan. This 
mirrors the profitability distinction already used for materialized-input 
dynamic partition pruning.
   
   Safely materialized caches use the new 
`spark.sql.optimizer.runtime.bloomFilter.materializedCreationSideThreshold` 
setting, which defaults to 100 MB. The existing 10 MB creation-side limit 
remains unchanged for all other inputs.
   
   ### How was this PR tested?
   
   Focused existing and new optimizer/cache suites were run with:
   
   ```bash
   build/sbt "sql/testOnly \
     org.apache.spark.sql.InjectRuntimeFilterSuite \
     org.apache.spark.sql.execution.columnar.InMemoryColumnarQuerySuite \
     org.apache.spark.sql.execution.columnar.ConcurrentInMemoryRelationSuite \
     org.apache.spark.sql.util.PartitionKeyedAccumulatorSuite"
   ```
   
   The coverage exercises selective and statistics-proven cached joins, 
projected join keys, size thresholds, join semantics, memory-only and partially 
loaded caches, unsafe expressions, random-IV encryption, permissive and 
preinitialized file readers, stale cache generations, duplicate partition 
completions, and partially consumed cache iterators.
   
   The standalone benchmark compares the same sort-merge join with runtime 
Bloom filtering disabled and enabled, verifies identical query results, and 
reports actual wide-side shuffle bytes:
   
   ```bash
   build/sbt "sql/Test/runMain \
     
org.apache.spark.sql.execution.benchmark.RuntimeBloomFilterCachedInputBenchmark"
   ```
   
   On 500,000 deterministic fact rows and a cached 1%-selective key set, the 
measured results were:
   
   ```text
   Fact-side shuffle without the runtime Bloom filter: 33,679,423 bytes
   Fact-side shuffle with the runtime Bloom filter:       344,110 bytes
   Fact-side shuffle reduction:                             98.98%
   
   Best query time without the runtime Bloom filter: 333 ms
   Best query time with the runtime Bloom filter:    178 ms
   ```
   
   The four existing affected suites passed all **67 tests**. Two additional 
existing cache-lifecycle regressions passed, covering accumulator cleanup after 
uncaching and materialization-bookkeeping reset after `clearCache`. Scala 
source and test style checks also passed for the Catalyst and SQL modules.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes. Eligible joins against safely materialized cached relations can now 
receive a runtime Bloom filter automatically and shuffle fewer rows. A new SQL 
configuration controls the maximum size of those cached filter inputs; all 
existing configuration defaults and public APIs retain their prior behavior.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: OpenAI Codex.
   


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