# 23 — Pagination [← Previous: 22 — Sorting](22-sorting.md) | [Next: 24 — Interceptors →](24-interceptors.md) | [Back to README →](../README.md) Backs the rewrite of ankurm.com post 4890 (pagination). ## `setFirstResult`/`setMaxResults` translate to the dialect's real syntax ```java query.setFirstResult(2); query.setMaxResults(2); ``` generates, on H2: ```sql ... order by a1_0.sequence offset ? rows fetch first ? rows only ``` Different dialects render this differently (`LIMIT ... OFFSET ...` on MySQL/PostgreSQL, `OFFSET ... FETCH ...` on SQL Server/H2, `ROWNUM` tricks on older Oracle) — the JPA-level API is the same everywhere; only the generated SQL shape changes. [`PaginationTest.java`](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) — [output](output/23-limit-offset.txt) ## `ScrollableResults` with `ScrollMode.FORWARD_ONLY` ```java try (ScrollableResults
results = session.createQuery(hql, Article.class) .setReadOnly(true) .scroll(ScrollMode.FORWARD_ONLY)) { while (results.next()) { Article a = results.get(); // process one row at a time } } ``` No `List
` holding every row is built by application code — rows are pulled from the JDBC `ResultSet` one at a time as `next()`/`get()` are called. Whether this actually avoids loading the whole result set into memory server-side too depends on the JDBC driver's own fetch-size behavior, not on this API alone. [Test](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) — [output](output/23-scrollable-forward-only.txt) ## `JOIN FETCH` + pagination: it depends on what you order by The often-repeated claim is that `join fetch` over a `to-many` association combined with `setFirstResult`/`setMaxResults` always falls back to loading everything into memory and paginating in application code, logging a warning. Tested directly against `hibernate-core-7.4.5.Final.jar`, this is only half true: - **Ordering by a column on the root entity** (`order by a.sequence`): Hibernate 7.4.5's query translator paginates a *derived subquery* of root ids first (its own `OFFSET`/`FETCH`), then joins the fetched collection onto that already-paginated set of ids. No in-memory fallback, no warning. - **Ordering by a column on the fetched collection itself** (`order by c.body`, where `c` is the joined collection alias): the "paginate the root ids first" trick can't work, because the sort key isn't a root-entity column. This is the query shape that reproduces the real in-memory fallback. ```sql -- root-ordered: paginates a derived subquery of ids, then joins select a1_0.id, c1_0.article_id, ... from ( select distinct a1_0.id, a1_0.sequence, a1_0.title from article a1_0 where ... order by a1_0.sequence offset ? rows fetch first ? rows only ) a1_0 join comment c1_0 on a1_0.id = c1_0.article_id order by a1_0.sequence ``` The warning's real message and code, read directly out of `QueryLogging.i18n.properties` in the jar: ``` HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory ``` Not `HHH000104`, a code sometimes quoted for this that belongs to a different, older message entirely — checked with `javap` against `QueryLogging_$logger.class`, not assumed from a search result. [Tests](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) — [root-ordered output](output/23-joinfetch-root-order-no-warning.txt), [collection-ordered output](output/23-joinfetch-collection-order-warning.txt) ## Keyset (seek) pagination ```sql select a from Article a where a.id > :lastId order by a.id asc ``` with `setMaxResults(pageSize)` and no `setFirstResult` at all. Each page's `WHERE` clause carries the previous page's last id, so the database never has to count-and-skip rows the way `OFFSET` does — the cost of fetching page 500 is the same as fetching page 1. [Test](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) — [output](output/23-keyset-seek.txt) ## The total-count-query pattern A "Page 2 of 7" UI needs two separate queries — a `COUNT` and a `LIMIT`/`OFFSET` `SELECT` — not one query doing both; SQL has no way to return a page of rows and the total matching count in a single result set without a window function trick most codebases don't bother with for this. [Test](../src/test/java/com/ankurm/hibernatedemo/pagination/PaginationTest.java) — [output](output/23-total-count-pattern.txt) ## Going deeper - Deep `OFFSET` pagination degrades because the database still has to *generate and discard* every skipped row before reaching the page — keyset pagination sidesteps this entirely, at the cost of not supporting arbitrary "jump to page N" navigation. - `Slice`/`Page` abstractions (Spring Data) wrap the count-query pattern automatically; knowing the two-query shape underneath explains why a `Pageable` with `unpaged()` sort still issues a `COUNT`. - [Hibernate ORM 7.4 User Guide — pagination](https://docs.jboss.org/hibernate/orm/7.4/userguide/html_single/Hibernate_User_Guide.html#pagination)