Does Hibernate paginate JOIN FETCH queries in memory instead of SQL?
0 reputation · 19 Dec 2021, 19:52 UTC
0 reputation · 19 Dec 2021, 19:52 UTC
I am designing a paginated endpoint in a Spring application using Hibernate ORM 6.x as the JPA provider, backed by PostgreSQL. The query loads parent entities together with a lazy collection, so I want to combine a JOIN FETCH with setFirstResult() and setMaxResults() on a TypedQuery.
My concern is the interaction between collection fetch joins and pagination. I have read that when a query fetches a collection, Hibernate may not be able to apply row limits in the generated SQL, because limiting rows would truncate the joined children. In that case it allegedly falls back to fetching all matching rows and applying the offset and limit in memory, which would defeat the purpose of pagination for large tables and risk high memory usage in the persistence context.
I would like to confirm the exact behavior before committing to this design, ideally verified against generated SQL with show_sql enabled rather than assumed from documentation summaries.
Specifically:
JOIN FETCH on a collection, and is a warning logged when this happens?List, Set, and bag-mapped collections?35275 reputation · 20 Dec 2021, 00:00 UTC
Yes. In Hibernate ORM 6.x, if a JPQL/HQL query contains a JOIN FETCH of a collection-valued attribute and you also call setFirstResult()/setMaxResults(), Hibernate does not apply the limit in SQL. It executes the full join, deduplicates the parent rows, and slices the result list in memory. It logs a warning (message id HHH000104, roughly: firstResult/maxResults specified with collection fetch; applying in memory) at WARN level when this happens.
A collection fetch join multiplies each parent row by the number of joined children. A SQL LIMIT 20 would cap joined rows, not parents — a parent with 20 children could consume the entire page and have its collection silently truncated. Since Hibernate cannot express "20 distinct parents" with a plain row limit on a joined result, it refuses to push pagination down and does it in Java instead.
LIMIT/OFFSET; all rows matching the WHERE clause are fetched; the WARN line is emitted; the returned list is sliced to the requested page.List (bag or ordered), Set, and other collection mappings. The mapping type changes how duplicates are handled in the result, not whether pagination moves to memory.spring.jpa.show-sql=true
logging.level.org.hibernate.orm.deprecation=WARN
logging.level.org.hibernate=WARNRun the query with setFirstResult(0) and setMaxResults(10) against a table with more than 10 matching parents. You should see (a) SQL with the join but no limit/offset clause, and (b) the HHH000104 warning. Repeat with List, Set, and bag mappings to confirm identical SQL shape.
Yes — splitting is the standard pattern. First paginate on parent IDs only (this query has no collection join, so LIMIT/OFFSET stay in SQL), then fetch the collections for just those IDs:
// Query 1: paginated, SQL-level
List<Long> ids = em.createQuery(
"select p.id from Parent p where ... order by p.id", Long.class)
.setFirstResult(offset).setMaxResults(size).getResultList();
// Query 2: fetch collections for the page only
List<Parent> parents = em.createQuery(
"select distinct p from Parent p left join fetch p.children "
+ "where p.id in :ids order by p.id", Parent.class)
.setParameter("ids", ids).getResultList();The second query has no pagination parameters, so no in-memory fallback is triggered, and the database only ever sees a bounded ID list. An entity graph on the second query is an equivalent alternative to the explicit fetch join.
ORDER BY (e.g., on the primary key). Without it, row order before in-memory slicing is undefined, so pages can overlap or skip rows even in the fallback path.limit in the logged SQL is cheap insurance.One missing detail only matters if you see different behavior: your exact Hibernate 6.x minor version and whether the query uses distinct explicitly — include the logged SQL and warning line if results diverge from the above.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.