Pagination & Sorting
Pagination & Sorting
When a table grows to thousands or millions of rows, loading every record into memory for a single request is both slow and wasteful. Spring Data JPA solves this cleanly with two abstractions — Pageable and Sort — and wraps the result in a rich Page<T> object that carries not only the data but also the navigation metadata your UI needs.
The Core Abstractions
Spring Data introduces three types that work together:
Sort— describes the ORDER BY clause: one or more properties each with a direction (ASCorDESC). Immutable and composable.Pageable— combines a zero-based page number, a page size, and an optionalSort. The most common implementation isPageRequest.Page<T>— the result of a paginated query. ExtendsSlice<T>and adds the total element and page counts, which requires a separateCOUNT(*)query.
Page<T> always issues a second COUNT query so it knows the total number of records and pages. Slice<T> skips the count and only knows whether a next slice exists. For infinite-scroll or cursor-based UIs, Slice is cheaper. For a traditional paginator showing "Page 3 of 47", you need Page.
Enabling Pagination in a Repository
Add Pageable as a parameter to any repository method and return Page<T> or Slice<T>. Spring Data JPA handles the rest — it injects the LIMIT and OFFSET (or equivalent) and, for Page, a wrapping COUNT query.
The JpaRepository base already provides findAll(Pageable), so you only need to declare it when you add filter parameters.
Building a PageRequest
Use the static factory methods on PageRequest:
PageRequest.of():
PageRequest.of(apiPage - 1, size).
Working with the Page Result
The Page<T> object contains everything a list UI needs:
In a Spring MVC or REST controller, you can expose this directly:
Spring's Jackson serializer turns Page<T> into a JSON envelope with content, totalElements, totalPages, number, and more — exactly what a front end needs to render a paginator.
Sorting Without Pagination
Sometimes you want ordered results without a page limit. Pass a Sort object directly:
Using @Query with Pagination
Custom JPQL queries also accept Pageable. Annotate the method with @Query and add a Pageable parameter as the last argument:
Spring Data injects the ORDER BY and LIMIT from Pageable into the generated SQL automatically. For the count query it wraps your JPQL in a SELECT COUNT(p) unless you provide a custom countQuery attribute.
Sort.by("description") on a TEXT column with millions of rows will force a full filesort. Index the columns you sort on — especially createdAt and status fields that are common sort targets in list UIs.
Web MVC Auto-Binding with Pageable
Spring MVC can bind Pageable directly from request parameters if you add @EnableSpringDataWebSupport to your configuration (it is enabled automatically in Spring Boot):
The sort parameter can appear multiple times for multi-column sorting. This lets the front end drive pagination and ordering without any boilerplate in the controller.
Performance Notes
- OFFSET cost:
LIMIT 20 OFFSET 10000still reads and discards 10,000 rows in most databases. For very deep pages, consider keyset (cursor) pagination instead. - COUNT query: Every
Pageresult fires two SQL queries. If the total count is expensive (e.g., joining several tables), switch toSliceand hide the total from users, or cache the count separately. - Fetch joins and pagination: Hibernate logs a warning when you mix a fetch join (which inflates rows) with
Pageable. It performs pagination in memory rather than in SQL, which can be catastrophic on large datasets. Fix it with a two-query approach: paginate IDs first, then fetch the full entities by those IDs.
Summary
PageRequest.of(page, size, sort) is your entry point. Pass it to any repository method that returns Page<T> and you get back data, navigation state, and total counts in one object. Use Sort.by() alone when you need ordering without page limits. Watch the two performance traps — deep OFFSET cost and fetch-join conflicts — and you will have a scalable, maintainable data layer.