Nimble Pagination in Hibernate: Lightweight Page‑by‑Row Navigation
A practical guide to implementing lightweight pagination with Hibernate's setMaxResults and setFirstResult, including COUNT coordination, version‑aware considerations, and offset performance risks.
28 Mar 2026, 04:26 UTC

Paginating through large datasets often forces an application to load every matching row before returning just the page the user sees. This wastes memory, inflates response time, and can even exhaust database connections. A nimble pagination strategy avoids that by fetching only the rows needed for the current page.
With Hibernate’s setMaxResults and setFirstResult you can navigate page‑by‑row without materializing the full result set, but you must pair the query with a COUNT check and watch offset depth.
Requirements for efficient pagination
- Defined page size that balances round‑trips and memory use
- Awareness of the underlying fetch plan and entity associations
- Ability to run a coordinating COUNT query against the same criteria
- Hibernate version knowledge (5 vs 6 optimization differences)
Smallest suitable design
The minimal change to enable page‑by‑row navigation is to replace a standard getResultList() call with a typed query that configures the first result and max results:
Query query = entityManager.createQuery("SELECT e FROM Entity e WHERE e.status = :status", Entity.class);
query.setFirstResult(20 * (3 - 1)); // zero‑based offset
query.setMaxResults(20);
List page = query.getResultList();Place the offset calculation pageSize * (pageNumber - 1) in a repository method or service layer where the page metadata is already available. No transaction‑level changes are required; the query executes within the existing persistence context.
Trust and data boundaries
Coordinating a COUNT query
Pagination metadata (total pages, total elements) depends on an accurate row count. Hibernate does not automatically translate setMaxResults/setFirstResult into a COUNT, so run a separate aggregate query:
Query countQuery = entityManager.createQuery("SELECT COUNT(e) FROM Entity e WHERE e.status = :status", Long.class);
long total = countQuery.getSingleResult();Compare total with the number of rows returned by the paginated query. If they differ, the page boundaries are misaligned.
Lazy fetch and associated entities
Hibernate’s default fetch type is lazy. When paginated results contain associated entities, accessing them after the session closes triggers additional SELECT statements. If the page response needs those entities, consider FetchMode.SUBSELECT or a joined query that respects the pagination limits.
Operational checks
Verifying row counts
- Execute the paginated query for a known page and page size.
- Count the elements in the returned list; it should equal
setMaxResults(or fewer at the last page). - Run the COUNT query from the same criteria set and confirm the total matches the pagination metadata.
Offset depth and performance
Large offset values—such as page 10 000 with a page size of 50—cause the database to scan and discard thousands of rows before returning the requested page. This degrades latency and can lead to timeouts. For deep pages, keyset (seek‑based) pagination using a unique, monotonically increasing cursor (e.g., created‑at timestamp plus primary key) avoids the offset penalty entirely.
| Hibernate version | setMaxResults interaction | Noted behavior |
|---|---|---|
| 5.x | Applies within the fetch plan; may affect association loading | Fetch‑plan‑aware; row count can diverge if fetch strategy is aggressive |
| 6.x | May employ different optimization strategies, including result‑set streaming | Row‑count accuracy depends on whether the query is wrapped in a subquery |
Failure modes and conditions that would change the design
- Deep paging without keyset: When offset exceeds a few thousand rows, latency spikes. Switch to keyset pagination or increase page size strategically.
- Entity association access: If lazy‑fetched entities cause N+1 queries inside the paginated block, perceived performance drops. Pre‑fetch or use a DTO projection that selects only needed fields.
- Hibernate version upgrade: Moving from 5 to 6 may change how
setMaxResultsinteracts with the query optimizer. After upgrading, re‑run the COUNT‑vs‑paginated‑query verification and adjust fetch plans if row counts shift. - Schema changes: Adding or removing columns that affect index usage can alter offset performance. Monitor query plans after DDL changes.
Practical verification checklist
- Run the paginated query with a concrete page size (e.g., 25) and offset (e.g., page 4 → offset 75).
- Collect the returned list and count its size; it must be ≤ the requested page size.
- Execute the identical
WHEREclause in aSELECT COUNT(*)query. - Confirm the COUNT result equals the expected total rows; use it to compute total pages.
- If the counts mismatch, review fetch plans, Hibernate version notes, and whether the
WHEREclause includes functions that prevent index usage.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.