Hibernate Pagination: Offset-based vs. Keyset-based for Large Datasets
0 reputation · 27 Nov 2020, 04:59 UTC
0 reputation · 27 Nov 2020, 04:59 UTC
When implementing pagination for tables containing millions of records in Hibernate, there is a trade-off between API simplicity and database performance. The standard approach utilizes setFirstResult() and setMaxResults(), which translates to SQL OFFSET and LIMIT clauses.
While offset-based pagination allows users to jump to any arbitrary page, it often results in performance degradation as the offset increases, because the database must scan and discard all preceding rows before returning the requested set. In contrast, keyset-based pagination (the seek method) filters results based on the last seen unique identifier, ensuring constant-time lookup regardless of the depth of the dataset.
However, keyset pagination restricts navigation to sequential movements (next/previous) and requires a strictly ordered, unique column to maintain consistency.
setFirstResult() outweigh the UX benefit of arbitrary page jumping?34775 reputation · 27 Nov 2020, 05:29 UTC
There is no universal row-count threshold where setFirstResult() becomes "too slow" — the cost scales with the offset value, not the table size. A 10-million-row table serves page 2 (offset 20) instantly, while a 200,000-row table can stall on offset 150,000. The practical rule: if users routinely page beyond offsets of roughly 10,000–50,000 rows, or your p95 query latency climbs past your API budget (commonly 100–300 ms) as pages deepen, keyset pagination wins. Below that, offset paging is fine and simpler.
setFirstResult(n) emits OFFSET n, and most databases must still read, sort, and discard those n rows before returning your page. Work grows linearly with page depth. Keyset pagination replaces the skip with a filter:
// Offset — degrades with depth
query.setFirstResult(page * size).setMaxResults(size);
// Keyset — constant cost, requires indexed unique key
List<Order> page = em.createQuery(
"SELECT o FROM Order o WHERE o.id > :lastId ORDER BY o.id ASC", Order.class)
.setParameter("lastId", lastSeenId)
.setMaxResults(size)
.getResultList();The seek predicate uses the index directly, so page 1 and page 100,000 cost the same.
No — Hibernate (including 6.x) ships no built-in hybrid pagination mode. Both styles are just queries you write yourself. The common application-level hybrid is: use offset paging for the first N pages (where jumping is cheap and users actually click page numbers), then switch to keyset/"load more" semantics beyond that, or expose only next/previous navigation. Some teams also precompute page-boundary keys in a background job to restore random access, but that is custom logic, not a Hibernate feature.
EXPLAIN ANALYZE at shallow and deep offsets; compare latency, not row counts.created_at), append the primary key as a tiebreaker: ORDER BY createdAt, id, and filter on both.Keyset requires a stable, unique, indexed ordering — changing the ORDER BY invalidates saved cursors, and concurrent inserts can shift results under either strategy. The latency thresholds above are heuristics from typical OLTP workloads, not measured guarantees; your storage engine, index coverage, and sort width move the crossover point. Measure on your own data before committing to a rewrite.
Use comments to ask for clarification. Post a solution as an answer.
5,940 reputation · 27 Nov 2020, 09:34 UTC
When using keyset pagination in Hibernate, the WHERE clause must exactly mirror the ORDER BY list to guarantee a deterministic, gap‑free scan. For a single‑column order you can write WHERE id > :lastId, but for a composite order such as ORDER BY col1 ASC, col2 ASC you need a tuple predicate like WHERE (col1, col2) > (:lastCol1, :lastCol2) or the expanded form WHERE col1 > :lastCol1 OR (col1 = :lastCol1 AND col2 > :lastCol2). This ensures the index can be used efficiently and prevents duplicates or missing rows when concurrent writes occur.