Hibernate Pagination: Transitioning from Offset-based to Keyset Seek for Deep Page Performance
0 reputation · 20 Aug 2024, 04:50 UTC
0 reputation · 20 Aug 2024, 04:50 UTC
When utilizing setFirstResult() and setMaxResults() in Hibernate ORM, the generated SQL typically relies on LIMIT and OFFSET clauses. While efficient for initial pages, this approach causes linear performance degradation as the offset increases because the database must scan and discard all preceding rows before returning the target window.
There is a known risk when combining pagination with join fetches, where Hibernate may perform pagination in memory rather than at the database level, potentially leading to OutOfMemoryError. Additionally, offset-based pagination is susceptible to result drifting if records are inserted or deleted between requests.
To maintain constant-time performance for large datasets, a transition to keyset pagination (the seek method) is required, filtering by the last seen unique identifier instead of a numeric offset.
setFirstResult()?31775 reputation · 20 Aug 2024, 16:11 UTC
Replace setFirstResult() with a WHERE clause that filters on the last seen unique sort key (the "seek" method). This eliminates the OFFSET clause and lets the database use an index seek, giving constant‑time page retrieval.
CriteriaBuilder and add them to a CriteriaQuery.ORDER BY on the same column, most relational databases generate an index seek rather than a full scan.join fetch for to‑one associations or @BatchSize for collections keeps pagination in the DB.CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Entity> cq = cb.createQuery(Entity.class);
Root<Entity> root = cq.from(Entity.class);
// Seek predicate: greater than the last seen key
Predicate seek = cb.greaterThan(root.get("id"), lastSeenId);
cq.where(seek);
// Order by the same key to maintain stable ordering
cq.orderBy(cb.asc(root.get("id")));
// Limit page size
TypedQuery<Entity> query = em.createQuery(cq);
query.setMaxResults(pageSize);
List<Entity> page = query.getResultList();
join fetch for needed to‑one associations before setting the seek predicate. For collections, prefer @BatchSize or a separate query to avoid duplicate rows that would force Hibernate to paginate in memory.lastSeenId for the subsequent request.hibernate.show_sql=true) and confirm the generated SQL lacks an OFFSET clause.EXPLAIN on the logged query; look for an "Index Seek" (or "Range Scan") on the sorted column.setFirstResult(10000); keyset should show flat latency.Missing diagnostic detail: If your sort key is not guaranteed to be unique and strictly monotonic (e.g., a non‑unique timestamp), keyset pagination could skip or duplicate rows. Confirm the uniqueness of the chosen key before proceeding.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.