Optimizing Large Dataset Retrieval in Hibernate: Keyset vs. Offset Pagination
Learn how to solve the 'deep paging' performance problem in Hibernate by replacing offset-based pagination with Keyset Pagination and using StatelessSession for bulk reads.
02 Oct 2026, 00:25 UTC

The Performance Wall of Offset Pagination
When retrieving large datasets in Hibernate, the standard approach of using setFirstResult() and setMaxResults() often leads to a performance collapse known as the "deep paging" problem. As the offset increases, the database must scan and discard all preceding rows before returning the requested page, turning a simple query into an $O(n)$ operation.
The takeaway: For tables exceeding 100,000 records, replace offset-based pagination with Keyset Pagination (also known as the Seek Method) to maintain constant query performance regardless of page depth.
Prerequisites
- Hibernate 5.x or 6.x configured with a relational database.
- An indexed column to serve as the unique identifier for the keyset (typically a primary key or a unique timestamp/ID combination).
- Administrative access to enable SQL logging for verification.
Implementing Keyset Pagination
Unlike offset pagination, which tells the database how many rows to skip, keyset pagination tells the database exactly where to start based on the last record of the previous page.
The Implementation Pattern
To implement this, you must track the value of the sorting column from the last item of the current page and use it as a filter in the WHERE clause of the next request.
// Example: Fetching the next page of 50 records after ID 12345
// Run this in your Service/DAO layer with standard Hibernate Session permissions
String hql = "FROM Order o WHERE o.id > :lastSeenId ORDER BY o.id ASC";
List<Order> results = session.createQuery(hql, Order.class)
.setParameter("lastSeenId", 12345)
.setMaxResults(50)
.getResultList();
Comparison: Offset vs. Keyset
| Feature | Offset Pagination | Keyset Pagination |
|---|---|---|
| SQL Mechanism | LIMIT 50 OFFSET 10000 |
WHERE id > 12345 LIMIT 50 |
| Performance | Degrades as page depth increases | Constant performance |
| Data Consistency | Items may shift if rows are inserted | Stable; no duplicate items across pages |
| Random Access | Supports jumping to page X | Only supports sequential (Next/Prev) |
Handling Memory Overhead with StatelessSession
When processing massive result sets for reports or exports, the Hibernate first-level cache (Persistence Context) can cause OutOfMemoryError because it tracks every entity loaded. For read-only pagination, use a StatelessSession.
A StatelessSession does not perform dirty checking or maintain a cache, significantly reducing memory overhead during bulk reads.
// Open a stateless session for bulk processing
try (StatelessSession stateless = sessionFactory.openStatelessSession()) {
ScrollableResults cursor = stateless.createQuery("FROM LargeLog l ORDER BY l.id")
.scroll(ScrollMode.FORWARD_ONLY);
while (cursor.next()) {
LargeLog log = (LargeLog) cursor.get(0);
// Process record without adding to persistence context
}
}
Verification and Diagnostics
To ensure your pagination is executing at the database level and not in memory, perform the following checks:
- Verify SQL Generation: Enable
hibernate.show_sql=truein your configuration. Ensure the logs show aWHEREclause (for keyset) or aLIMIT/OFFSET(for offset) rather than a full table scan. - Memory Monitoring: Use a profiler (like VisualVM) to monitor heap usage. If memory climbs linearly as you iterate through pages, you are likely using a standard
Sessioninstead of aStatelessSessionor failing to callsession.clear(). - Execution Timing: Compare the time to fetch page 1 versus page 1,000. In offset pagination, page 1,000 will be significantly slower. In keyset pagination, the time should remain nearly identical.
Critical Limitations
- OneToMany Joins: Avoid using
setFirstResultwhen joining collections (OneToMany). Hibernate cannot perform this pagination in SQL for joined collections and will instead fetch the entire result set into memory to paginate it in Java, leading to immediate performance failure. - Sorting Requirements: Keyset pagination requires a deterministic sort order. If you sort by a non-unique column (e.g.,
user_name), you must add a unique tie-breaker (e.g.,ORDER BY user_name, id) to avoid skipping records.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.