skip to content

What is the difference between returning a Page<T> and a Slice<T> from a Spring Data repository method?

level: juniorimportance: must knowfreq 70%

answer

  1. Page extends Slice
  2. Page = data query + COUNT query
  3. Slice = one query, fetches limit+1
  4. hasNext via extra row
  5. no total -> use Slice

basics

~20 s

A Page knows the total number of results (total elements and total pages), so it runs an extra COUNT query. A Slice only knows whether there is a next page, so it skips the COUNT query and is cheaper.

solid answer

~40 s

Both are chunks of a larger result set requested via a Pageable. Page<T> extends Slice<T> and adds getTotalElements() and getTotalPages(), which force Spring Data to run a second, separate SELECT COUNT(*) query on top of the data query. Slice<T> exposes only hasNext()/hasPrevious() plus the content, so it never issues a count. To answer hasNext() without a count, Spring Data fetches pageSize + 1 rows, and if the extra row exists it reports hasNext()=true and trims it off. Use Page when the UI needs total counts or numbered pages; use Slice for infinite-scroll or 'load more' where you only need to know if more data exists, avoiding the count cost on large tables.

code

java · 16 lines
java
public interface UserRepository extends JpaRepository<User, Long> {

    // Two queries: SELECT ... LIMIT ? OFFSET ?  +  SELECT COUNT(*) ...
    Page<User> findByActiveTrue(Pageable pageable);

    // One query only: fetches pageSize + 1 rows to compute hasNext()
    Slice<User> findByDepartment(String department, Pageable pageable);
}

// Usage
Page<User> page = repo.findByActiveTrue(PageRequest.of(0, 20, Sort.by("id")));
long total = page.getTotalElements();   // available on Page
boolean more = page.hasNext();          // available on both

Slice<User> slice = repo.findByDepartment("ENG", PageRequest.of(0, 20, Sort.by("id")));
boolean hasMore = slice.hasNext();      // no getTotalElements() here

go deeper

for a junior

Must state that Page gives totals and does an extra count query, while Slice only tells you if there's a next page.

for a middle

Should explain the limit+1 trick and pick the right type for infinite scroll vs numbered pages.

for a senior

Should discuss count cost on large/joined tables and the shared OFFSET limitation of both.

for a principal

Should connect to system design: when count cost forces Slice, and when even Slice's OFFSET is too slow, pushing toward keyset scrolling.

## The domain When you paginate in Spring Data you pass a `Pageable` (usually `PageRequest.of(pageNumber, pageSize, Sort)`) and the repository returns a chunk. There are three return types that describe that chunk: `List<T>` (just the rows), `Slice<T>`, and `Page<T>`. ## Slice<T> `Slice<T>` is the base abstraction. It carries: - `getContent()` — the rows in this chunk, - `getNumber()`, `getSize()`, `getNumberOfElements()` — where you are and how big the chunk is, - `hasNext()` / `hasPrevious()` — navigation, - `getSort()`. Crucially it does **not** know the total number of matching rows. To answer `hasNext()` cheaply, Spring Data internally requests **`pageSize + 1`** rows (it applies `LIMIT pageSize+1`). If it gets that extra row back, it knows another chunk exists, sets `hasNext()=true`, and removes the extra element from the content it returns to you. So a `Slice` costs **one** query. ## Page<T> `Page<T>` *extends* `Slice<T>` and adds: - `getTotalElements()` — total matching rows across all pages, - `getTotalPages()` — derived from totalElements and pageSize. To populate those, Spring Data runs a **second, separate query**: a `SELECT COUNT(...)` with the same `WHERE`/join filters but without paging or sorting. So a `Page` costs **two** queries: the data query plus the count query. (Optimization: for derived/`@Query` methods Spring may skip the count when the result clearly fits on one page — e.g. first page and fewer than pageSize rows returned — but you should not rely on this.) ## Why the count is expensive On a large or heavily-joined table, `COUNT(*)` with a complex `WHERE` can scan many rows or indexes and can rival or exceed the cost of the data query itself. Doing it on every page request adds up. If the UI never shows a total or a page count, that work is pure waste — that is exactly when `Slice` wins. ## When to use which - **Page** — classic paginated tables/grids that show 'Page 3 of 47' or a total-results badge; when the client needs to jump to an arbitrary page number. - **Slice** — infinite scroll, 'Load more' buttons, mobile feeds: you only need 'is there more?'. - **List** — you don't need navigation metadata at all, just the rows for a given `Pageable`. ## Gotchas - `Page` extends `Slice`, so a method returning `Page` can be assigned to a `Slice` variable, but you still paid for the count. - Sorting matters: without a stable `Sort` (ideally including a unique tiebreaker), rows can shift between chunks and you may see duplicates or gaps — true for both Page and Slice. - Both still use `OFFSET` under the hood, so deep pages (large offsets) remain slow regardless — that's a separate problem solved by keyset scrolling (`Window`/`ScrollPosition`).

  • How does a Slice determine hasNext() without running a count query?
    It asks the database for pageSize + 1 rows. If the extra (n+1th) row comes back, hasNext() is true; Spring Data then strips that extra row from the returned content so you still get pageSize rows.
  • If you never call getTotalElements(), does Page still run the count query?
    Yes. The count is executed when the Page is materialized, not lazily on the getter (for standard JPA repository queries). So choosing Page already commits you to the count regardless of whether you read the total.

saying these in an interview costs you the question

  • Claiming Slice runs a count query too
  • Saying Page and Slice cost the same number of queries
  • Thinking getTotalElements() is available on Slice
  • Believing the count only runs when you call getTotalElements()

context