Hibernate pagination with setFirstResult() triggers in‑memory paging warning
0 reputation · 19 Dec 2020, 18:15 UTC
0 reputation · 19 Dec 2020, 18:15 UTC
In a Java application using Hibernate 6, the goal is to retrieve a subset of a large @OneToMany collection via JPQL pagination, using setFirstResult() and setMaxResults(). The application must avoid loading the entire collection into memory and maintain acceptable query performance.
Constraints include a large offset value (e.g., setFirstResult(10_000)) and the use of JOIN FETCH to eagerly load related entities. When both are combined, the Hibernate logs emit the warning HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!, indicating that the database cannot apply the limit and the JVM must perform in‑memory pagination.
This behavior varies across Hibernate versions and SQL dialects; some databases translate LIMIT/OFFSET differently, while others may support OFFSET/FETCH. The unresolved decision is whether to modify the query strategy to avoid the warning, or to accept the memory cost and rely on JVM tuning.
Questions:
JOIN FETCH with setFirstResult() always lead to in‑memory paging in Hibernate 6, or is it dependent on the underlying database dialect?Using JOIN FETCH together with setFirstResult() does not depend on the database dialect; the warning HHH000104: firstResult/maxResults specified with collection fetch; applying in memory! appears because the join produces multiple rows per root entity, so Hibernate cannot safely push the LIMIT/OFFSET to SQL and falls back to in‑memory pagination.
JOIN FETCH, one parent row can generate N child rows, making the total result set larger than the number of distinct parents.LIMIT/OFFSET at the database level will return the correct page of parents.// Query that returns only the IDs of the parent entities
String idQuery = "SELECT p.id FROM Parent p WHERE …";
List ids = entityManager.createQuery(idQuery, Long.class)
.setFirstResult(offset)
.setMaxResults(pageSize)
.getResultList();
if (!ids.isEmpty()) {
String parentQuery = "SELECT DISTINCT p FROM Parent p " +
"JOIN FETCH p.children WHERE p.id IN :ids";
List page = entityManager.createQuery(parentQuery, Parent.class)
.setParameter("ids", ids)
.getResultList();
// page now contains the requested slice with children eagerly loaded
}
// After loading the ID page, initialize collections in batches
entityManager.createQuery("SELECT p FROM Parent p WHERE p.id IN :ids", Parent.class)
.setParameter("ids", ids)
.setHint("org.hibernate.fetchSize", 50) // or use @BatchSize
.getResultList();
hibernate.generate_statistics=true
hibernate.show_sql=true
hibernate.format_sql=true
jdbcConnections and prepareStatementCount with the number of entities returned for a given setFirstResult value.EXPLAIN ANALYZE to see how many rows are scanned before the LIMIT/OFFSET is applied.Missing diagnostic detail that could change the recommendation: Do you have a stable, unique, indexed column suitable for keyset pagination? If yes, keyset pagination may replace the two‑step ID query and further reduce overhead.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 20 Dec 2020, 03:07 UTC
One effective alternative to the two-step ID fetch is replacing JOIN FETCH with @BatchSize on the collection mapping. When you use BatchSize, Hibernate can translate setFirstResult() and setMaxResults() directly into database SQL because the initial query no longer produces a Cartesian product via a join.
As you iterate through the paginated list of parents, Hibernate will fetch the related collections in batches (defined by the size) rather than one query per entity. This avoids the HHH000104 warning and maintains high performance by preventing the N+1 problem without exhausting JVM memory.
JOIN FETCH from the JPQL query.@BatchSize(size = 20) to the @OneToMany collection.LIMIT/OFFSET.