Careeroza
Pagination and Efficient Queries
Never Return Everything
GET /api/products returning 200,000 rows will use huge memory and take seconds. Always paginate list endpoints.
@GetMapping("/api/products")
public Page<ProductDto> list(@RequestParam(required = false) String category,
@PageableDefault(size = 20, sort = "name") Pageable pageable) {
return productService.search(category, pageable);
}
// repository: Spring Data builds LIMIT/OFFSET and a COUNT query
Page<Product> findByCategoryName(String category, Pageable pageable);Example
curl -s "localhost:8080/api/products?category=mobiles&page=1&size=2&sort=price,desc"Example output
{"content":[{"id":9,"name":"Galaxy S25","price":79999.00},{"id":3,"name":"Pixel 9","price":54999.00}],
"page":{"size":2,"number":1,"totalElements":38,"totalPages":19}}Tip: Cap the page size (spring.data.web.pageable.max-page-size=100) so nobody can ask for size=1000000. Pagination details are covered in the Hibernate & JPA material.
Offset vs Keyset (Seek) Pagination
OFFSET 100000 makes the database read and throw away 100,000 rows. For infinite-scroll feeds, use keyset pagination: "give me 20 items after the last id I saw".
Example
Both methods return the same page, but keyset touches far fewer rows on deep pages:
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> ids = IntStream.rangeClosed(1, 100_000).boxed().toList(); // product ids, sorted
int size = 5;
// OFFSET: skip page * size rows
int page = 10_000;
int[] scanned = {0};
List<Integer> offsetPage = ids.stream().peek(x -> scanned[0]++)
.skip((long) page * size).limit(size).toList();
System.out.println("offset page " + page + ": " + offsetPage + " rows read=" + scanned[0]);
// KEYSET: WHERE id > lastSeenId ORDER BY id LIMIT 5 (an index jumps straight there)
int lastSeenId = 50_000;
int start = Collections.binarySearch(ids, lastSeenId) + 1; // index lookup
List<Integer> keysetPage = ids.subList(start, start + size);
System.out.println("keyset after " + lastSeenId + ": " + keysetPage + " rows read=" + size);
}
}Output
offset page 10000: [50001, 50002, 50003, 50004, 50005] rows read=50005 keyset after 50000: [50001, 50002, 50003, 50004, 50005] rows read=5
@Query("select p from Product p where p.id > :afterId order by p.id")
List<Product> nextPage(@Param("afterId") Long afterId, Limit limit); // Spring Data 3.2+: Limit.of(20)Other Quick Wins
- Return DTO projections instead of whole entities for lists.
- Fix N+1 queries with
JOIN FETCHor@EntityGraph(see Hibernate & JPA). - Add database indexes for columns used in
WHEREandORDER BY. - Turn on
spring.jpa.properties.hibernate.generate_statistics=truein a test run to count queries per request.