viirya commented on PR #58895:
URL: https://github.com/apache/spark/pull/58895#issuecomment-6064730845
Thanks for landing this, @peter-toth. A few follow-ups after reading the
merged version. None of them is a correctness concern.
**Where the feature can pay off in practice**
I think the set of real workloads this helps is narrower than the benchmark
suggests, and it would help to say so, or to measure it, before anyone
considers changing the default.
- **The bloom has to exist first.** `InjectRuntimeFilter` only injects one
when the join is probably a shuffle join, or the application side already has a
shuffle below it, and the application-side scan is at least 10GB by default. A
typical fact table broadcast-joined with a filtered dimension gets no bloom, so
it gets nothing from this PR.
- **The survivors need physical locality.** A page is the unit that can be
skipped, and with default writer settings a narrow column's page holds up to
20,000 rows. With survivors spread at random at rate p, a page has no survivor
with probability about e^(-p·20000), so page skipping needs p well below
1/20,000, and the bloom's false positives count toward p. In practice the gain
comes from row groups or runs of pages with no survivor at all. That needs the
surviving keys to form a range in the table's physical order, or each key to
own whole pages. Hash bucketing does not give that. The most natural candidate,
a date key, is usually handled already: by DPP when the table is partitioned on
it, and by broadcast when the date dimension is small.
- **The tables most likely to be clustered on a join key are out of reach.**
Delta (Z-order, liquid clustering) reads through `DeltaParquetFileFormat`, a
subclass that the exact-class check excludes. Iceberg (sort orders) goes
through DSv2. The conf doc says "V1 Parquet file source", and a Delta user will
likely read that as covering them. It may be worth stating the subclass
exclusion there, and later offering subclasses an explicit opt-in.
- **The benchmark sits at the favorable end of all of this, and on local
storage.**
- The fact table is `spark.range` written as a single file, so it is fully
sorted on `k`.
- The "clustered" dimension selects one contiguous key range.
- Row groups are capped at 1,500 and 42,000 rows (~3MB). With the default
128MB the whole ~100MB table would be a single row group, so the clustered
cases' 33-of-34 row-group skips would not happen.
- The bloom is sized at 13 bits per item, injection is forced (broadcast
off, application-side threshold 0), and it runs on `local[1]` against a local
filesystem.
The representative rows are "scattered" with default pages: the join is
1.0–1.4x on the wide table and 0.9–1.0x on the narrow one.
My main worry is object storage, where most of these tables live. There, a
plain read fetches a row group's columns in one vectored read. This reader
issues two, phase 1 for the keys and phase 2 for the rest, and with no pushed
data filter phase 2 also reads the offset indexes first. For a row group where
the filter prunes little, that is extra request latency with nothing saved, and
a local SSD hides it. So the scattered rows above may look worse there, and the
clustered ones may gain less than the byte counts suggest. If you get a chance,
a run on object storage would tell us a lot, ideally TPC-DS (or similar) at a
realistic scale with default write settings and default injection thresholds,
noting which queries get `StorageFilters` and how they change.
**Survivor buffer memory**
The survivor buffer (up to 64MB per reader, so N times that per executor)
sits outside any `MemoryConsumer`. Registering it would let an allocation be
denied, and `spill()` could simply release the buffer and fall back to
re-reading the keys.
**Comments**
About 1,000 of the ~2,400 added non-test lines are comments, much of it
narrative (alternatives considered, past measurements, the same point restated
across `SQLConf`, `ParquetStorageFilter` and the reader). A cleanup that keeps
only the invariants and non-obvious trade-offs would make the reader much
easier to maintain.
**The public conf doc**
`spark.sql.parquet.storageFilterPushdown.enabled` is user-facing, but its
doc is ~25 lines of internals: phases, reading key columns twice, when the
reader gives up. I'd keep only what changes a user's decision or answer.
Something like:
> If true, the vectorized reader of the V1 Parquet data source may use a
runtime filter on join keys, such as a runtime bloom filter, to skip reading
the pages and row groups of other columns that hold no matching rows. The
filter is still applied after the scan, so results do not change, with two
exceptions: with spark.sql.files.ignoreCorruptFiles=true the rows returned from
a corrupt file can differ, and a file with an incorrect Parquet page index can
return wrong results (set parquet.filter.columnindex.enabled=false to stop
relying on it). Only ParquetFileFormat itself is supported, not its subclasses.
It can be slower when matching rows are spread across most pages. Matching key
values are buffered per row group, up to
spark.sql.parquet.storageFilterPushdown.maxSplicedRowGroupBytes, outside
Spark's memory manager.
--
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]