Skip to content

[SPARK-59056][SQL] Pre-size HashSets in ParquetFilters IN/InSet pushdown - #58349

Open
david-mollitor-db wants to merge 1 commit into
apache:masterfrom
david-mollitor-db:SPARK-59056
Open

[SPARK-59056][SQL] Pre-size HashSets in ParquetFilters IN/InSet pushdown#58349
david-mollitor-db wants to merge 1 commit into
apache:masterfrom
david-mollitor-db:SPARK-59056

Conversation

@david-mollitor-db

@david-mollitor-db david-mollitor-db commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Parquet IN/InSet pushdown builds a java.util.HashSet for each pushed-down
IN predicate, then adds exactly values.length elements to it. The sets are
created without an initial capacity, so they start at the default capacity (16)
and rehash as they grow, reallocating the internal bucket array several times
for large IN lists.

This PR pre-sizes those sets from the known element count. A java.util.HashSet
resizes once it is more than 75% full, and its int constructor treats the
argument as bucket capacity rather than expected size, so new HashSet(values.length)
would still rehash. To size correctly using only the JDK, a small helper
SparkCollectionUtils.newHashSetWithExpectedSize is added that derives the
initial capacity as expectedSize / 0.75 + 1, guaranteeing the set holds all
elements without a resize.

The helper is applied at:

  • the 12 sites in ParquetFilters.makeInPredicate, and
  • TypedParquetFilterOps.makeIn, the IN pushdown for the Types-Framework
    Time / timestamp-nanos types.

Why are the changes needed?

makeInPredicate is only reached when the IN list size exceeds
spark.sql.parquet.pushdown.inFilterThreshold (default 10); smaller lists take
a per-element equality OR-chain and never build a HashSet. So by construction
the set always holds more than the threshold's worth of values, and for larger
IN lists (dozens to thousands) the default-capacity set rehashes several times
per pushed-down predicate, allocating and discarding bucket arrays, once per
Parquet file split scanned. Pre-sizing removes that avoidable work. The change
is capacity-only and behavior-preserving.

Does this PR introduce any user-facing change?

No.

How was this patch tested?

Existing ParquetV1FilterSuite / ParquetV2FilterSuite, which cover IN/InSet
predicate pushdown (including the Time / timestamp-nanos types that exercise
TypedParquetFilterOps.makeIn), pass. The change is capacity-only, so predicate
semantics are unchanged.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Opus 4.8

@uros-b

uros-b commented Aug 30, 2026

Copy link
Copy Markdown
Member

+1, LGTM

@david-mollitor-db

Copy link
Copy Markdown
Contributor Author

@uros-b Is this good to merge?

@uros-b
uros-b self-requested a review September 8, 2026 15:20

import scala.jdk.CollectionConverters._

import com.google.common.collect.Sets

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I don't think we should introduce Guava Sets here... Please see that Spark already banned com.google.common.collect.Sets in Java checkstyle (SPARK-53231 / SPARK-53217).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cited precedent Maps.newHashMapWithExpectedSize is a different class and is not banned. Scala is not covered by that checkstyle rule, which is why CI still passed, but it is still the wrong direction.

@uros-b

uros-b commented Sep 8, 2026

Copy link
Copy Markdown
Member

Requesting changes in order to prevent accidental merge.

case ParquetByteType | ParquetShortType | ParquetIntegerType =>
(n: Array[String], values: Array[Any]) =>
val set = new HashSet[Integer]()
val set = Sets.newHashSetWithExpectedSize[Integer](values.length)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On another note, there is a completeness gap. TypedParquetFilterOps.makeIn is still unsized. Current master extracted Time / timestamp-nanos IN pushdown into TypedParquetFilterOps.makeIn:

override def makeIn(columnPath: Array[String], values: Array[Any]): FilterPredicate = {
  val set = new HashSet[T]()
  values.foreach(v => set.add(toPhysicalOrNull(v)))
  FilterApi.in(column(columnPath), set)
}

@uros-b uros-b left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider other approaches here.

Not sure what is the best path forward, but we can consider something like just keeping java.util.HashSet:

new HashSet(values.length)

which would already remove almost all resizes (Java 8+ HashMap rounds capacity to a power of two).

Alternatively, we can consider to size it with the load factor:

new HashSet[Integer]((values.length / 0.75f).toInt + 1)

which is the same formula Guava’s helper uses for expectedSize >= 3. The 0.75 formula only avoids at most one extra rehash.

@uros-b

uros-b commented Sep 8, 2026

Copy link
Copy Markdown
Member

In any case, impact is real but fairly small...

Parquet IN/InSet pushdown builds a `java.util.HashSet` for each pushed-down IN
predicate and then adds exactly `values.length` elements. The sets start at the
default capacity (16) and rehash as they grow, reallocating the internal bucket
array several times for large IN lists. Pre-size each set from the known element
count so it holds all IN values without rehashing.

A `java.util.HashSet` resizes once it is more than 75% full, and its int
constructor treats the argument as bucket capacity rather than expected size, so
`new HashSet(values.length)` would still rehash. Add a JDK-only
`SparkCollectionUtils.newHashSetWithExpectedSize` helper that derives the initial
capacity as `expectedSize / 0.75 + 1`, and use it at the 12 sites in
`ParquetFilters.makeInPredicate` and in `TypedParquetFilterOps.makeIn` (the
Time / timestamp-nanos IN pushdown), so every IN-pushdown set is sized up front.

The change is capacity-only and preserves the pushdown semantics.

Generated-by: Claude Opus 4.8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants