skip to content

In plain JPA/Hibernate (no framework helpers), how do you return only one page of rows from a JPQL query, and what SQL does Hibernate actually send to the database?

level: juniorimportance: must knowfreq 62%

answer

  1. setFirstResult = 0-based offset
  2. setMaxResults = page size
  3. Dialect LimitHandler rewrites SQL
  4. total ORDER BY + id tie-breaker
  5. deep offset = rows walked and discarded

basics

~20 s

Call setFirstResult(offset) and setMaxResults(pageSize) on the TypedQuery. Hibernate appends the dialect's limit clause (LIMIT ... OFFSET ..., or OFFSET ... FETCH FIRST), so the database returns only that page. Always pair it with a deterministic ORDER BY.

solid answer

~50 s

Pagination in plain JPA is two setters on `Query`/`TypedQuery`: - `setFirstResult(n)` — zero-based index of the first row to return. - `setMaxResults(k)` — maximum rows to return. Hibernate does not slice the list in Java: its `Dialect`/`LimitHandler` rewrites the SQL. On PostgreSQL and MySQL you get `... limit ? offset ?`; on modern SQL Server and Oracle you get `offset ? rows fetch next ? rows only`; older Oracle gets a `rownum` subselect wrapper. So the database reads and returns only the page. Two rules go with it. First, the query needs an **ORDER BY that is total** — add a unique tie-breaker such as the id — otherwise row order is undefined and pages can overlap or skip rows. Second, offsets are not free: `offset 100000` still makes the database walk and discard 100,000 rows. The same two setters work for Criteria queries and native queries. They stop pushing the limit into SQL only when the query fetch-joins a collection.

code

java · 7 lines
java
TypedQuery<Order> q = em.createQuery(
        "select o from Order o where o.status = :st order by o.createdAt desc, o.id desc",
        Order.class);
q.setParameter("st", Status.OPEN);
q.setFirstResult(pageNumber * pageSize);   // zero-based
q.setMaxResults(pageSize);
List<Order> page = q.getResultList();

go deeper

for a junior

Know the two setters, that firstResult is zero-based, and that an ORDER BY is required. Being able to name the generated LIMIT/OFFSET is enough.

for a middle

Explain that the dialect pushes the limit into SQL, why the ordering must be total, and that a separate count query is needed for totals.

for a senior

Add the cost model: deep offsets scan and discard, offsets drift under concurrent writes, page size must be capped, and collection fetch joins break SQL-level paging.

for a principal

Frame paging as an API contract decision — page numbers versus cursors, what stability guarantee you promise callers, and where you enforce limits across services.

## The problem A list screen or an export almost never wants every row a query matches. Pagination means: return rows *k* through *k+n* of a defined ordering, and let the database do the skipping rather than the application. ## The API JPA puts pagination on `jakarta.persistence.Query`, so it is available to JPQL/HQL queries, Criteria queries and native queries alike: - `setFirstResult(int)` — the **zero-based** index of the first row returned. `setFirstResult(0)` is the first row. A very common off-by-one bug is passing a one-based page number straight through; the conversion is `firstResult = (pageNumber - 1) * pageSize` for one-based UI page numbers. - `setMaxResults(int)` — the maximum number of rows returned. Fewer come back on the last page; an empty list comes back past the end. Both return the query so they chain, and both are ignored by `getSingleResult()` semantics only in the sense that the limit still applies to the underlying fetch. ## What Hibernate emits Hibernate does **not** load everything and sublist it. Each `Dialect` owns a `LimitHandler` that rewrites the generated SQL for that database: - PostgreSQL, MySQL, MariaDB, H2: `select ... order by ... limit ? offset ?` - SQL Server 2012+, Oracle 12c+, DB2, and the SQL:2008 standard form: `offset ? rows fetch next ? rows only` - Legacy Oracle: a nested `rownum` subselect wrapper The parameters are bound, not inlined, so the statement stays cacheable in the driver and the database plan cache. The consequence that matters: only the page's rows cross the network and only the page's rows become entities in the persistence context. ## ORDER BY is part of pagination, not a nicety SQL result order is undefined without `ORDER BY`. Two calls for "page 1" and "page 2" are two independent statements; the database may legitimately return rows in different orders (different plan, parallel scan, heap changes), so rows can appear twice or never. Worse, the ordering must be **total**: if you sort by `createdAt` and 50 rows share a timestamp, the split between pages inside that block is arbitrary. Add a unique tie-breaker: `order by o.createdAt desc, o.id desc` ## The cost model of offsets `limit 20 offset 200000` is not "jump to row 200000". The database must produce and discard the first 200,000 rows of the ordering (unless an index lets it skip-scan, and even then it counts them). Latency therefore grows with page depth. Shallow paging is fine; deep paging over large tables is the motivation for keyset/seek paging. A second, subtler issue is **drift**: with offset paging, rows inserted or deleted between page requests shift the window, so a user scrolling can see the same row twice or miss one entirely. ## Where the simple recipe silently stops working If the JPQL contains `join fetch` on a **collection** (`left join fetch o.items`), one logical entity spans several SQL rows, so a SQL `LIMIT` would cut a parent's collection in half. Hibernate refuses to do that: it drops the limit from the SQL, runs the whole query, builds every matching parent in memory, and slices afterwards — logging a warning. That is the classic trap and is treated separately. Fetch-joining a **to-one** association (`join fetch o.customer`) does not multiply rows, so paging stays in SQL. ## Counting `setMaxResults` tells you nothing about the total; a separate `select count(...)` query is needed for a page count, and it must not carry the `ORDER BY` or any `join fetch`. ## Practical hardening - Cap `maxResults` server-side (e.g. 200) and reject or clamp anything larger, so a caller cannot ask for a million rows. - Validate that `firstResult >= 0`. - Log or assert the ordering is total in code review. - Prefer a stable business ordering plus id, not `order by rand()`-style ordering, which cannot be paginated coherently at all.

  • Why must the ORDER BY include a unique column?
    Sorting only by a non-unique column leaves the order of tied rows undefined, and the database is free to return them differently on each execution. Page 1 and page 2 are separate statements, so a tied row can appear on both pages or on neither. Appending the primary key makes the ordering total and the split between pages deterministic.
  • Does setMaxResults limit rows in Java or in SQL?
    In SQL. The dialect's LimitHandler rewrites the statement with LIMIT/OFFSET or OFFSET ... FETCH FIRST, so only the page's rows leave the database. The one documented exception is a query that fetch-joins a collection, where Hibernate removes the limit from the SQL and paginates the materialised result in memory instead.

saying these in an interview costs you the question

  • Thinking setFirstResult is one-based, so page 1 skips a row
  • Believing Hibernate loads all rows and sublists them in Java for a normal query
  • Paginating without ORDER BY, or ordering only by a non-unique column
  • Assuming deep offsets are cheap because 'the database jumps straight to the row'
  • Accepting an unbounded page size from client input

context