Hibernate Pagination and Collection Fetching Memory Warning
0 reputation · 09 Oct 2023, 00:32 UTC
0 reputation · 09 Oct 2023, 00:32 UTC
When implementing pagination using setFirstResult() and setMaxResults(), Hibernate typically delegates the limit and offset logic to the database dialect. However, this behavior changes when the query involves join fetching of collections.
The goal is to retrieve a paginated subset of entities while simultaneously loading their associated collections to avoid N+1 select issues. The constraint is that Hibernate may trigger a HHH000104 warning, indicating that pagination is being applied in-memory rather than at the database level.
setMaxResults value before the in-memory filter is applied?33775 reputation · 09 Oct 2023, 11:00 UTC
Hibernate issues HHH000104 and falls back to in-memory pagination when you combine setFirstResult() and setMaxResults() with a fetch join on a collection-valued association, typically OneToMany or ManyToMany mapped as List, Set, or Bag. A fetch join on single-ended associations such as ManyToOne or OneToOne normally does not trigger this warning because those joins do not multiply root rows.
The cause is relational: joining a collection produces one database row per child element. A database LIMIT or OFFSET could cut through a parent's collection, returning a partial collection or losing the parent entirely. To avoid that, Hibernate loads the full matching result set and applies the page boundary in the JVM.
| Item | Status |
|---|---|
| HHH000104 appears with collection fetch joins plus pagination | Well-established Hibernate ORM behavior |
| Database LIMIT/OFFSET is suppressed when the warning fires | Likely; verify with SQL logging for your dialect and version |
| Exact warning text and trigger vary by Hibernate ORM version | Version-dependent; check your dependency tree |
When in-memory pagination applies, setMaxResults() does not bound the rows the database returns. Hibernate hydrates every matching parent row and its fetched collection into the persistence context, then discards everything outside the requested page. Heap pressure therefore scales with total matching rows multiplied by average collection size, not with the page size. Duplicate parent rows from the cartesian product add transient object overhead before de-duplication. If the transaction or session stays open, the first-level cache retains those entities until the session closes, extending retention and increasing GC pressure or OutOfMemoryError risk.
hibernate.show_sql and hibernate.format_sql) and reproduce the query. Confirm whether the SQL contains LIMIT/OFFSET and whether HHH000104 appears.setFirstResult() and setMaxResults() so the database applies the limit.@BatchSize, Hibernate.initialize(), or a second batch query keyed by the returned parent IDs.Check the Hibernate ORM version in your dependency tree, because in-memory pagination behavior is version-sensitive. Run the same page against thousands of parents with large collections, with and without the collection fetch join, and compare heap usage using a profiler or -XX:+HeapDumpOnOutOfMemoryError.
Missing diagnostic detail: which Hibernate ORM version you are using. Share it if the SQL log does not clearly show whether LIMIT/OFFSET is being pushed to the database.
Use comments to ask for clarification. Post a solution as an answer.
33,775 reputation · 09 Oct 2023, 04:12 UTC
In Hibernate 5.x and 6.x, a query that contains a JOIN FETCH on a collection cannot safely add a LIMIT/OFFSET in the generated SQL. The dialect therefore loads all matching rows into memory and then applies setFirstResult()/setMaxResults() in Java, which triggers the HHH000104 warning.
Because the entire result set—including all fetched collections—is materialized before the pagination filter, the JVM heap must hold every root entity and its collection rows, even if many will be discarded. This can increase GC pressure and, with large collections, lead to OutOfMemoryError.
To keep pagination at the database level, replace the collection fetch join with a @Fetch(FetchMode.SUBSELECT) or use @BatchSize so Hibernate issues a separate SELECT for the collection.