5.4 N+1 Problem & Optimization
← Back to Chapter 5: Spring Data JPA, Hibernate & Database
The N+1 Query Problem is the single most common performance bottleneck in Hibernate and JPA applications. It occurs when loading N parent entities results in N additional round-trip queries to fetch related child entities.
1. How the N+1 Problem Manifests
Consider a Department with a collection of Employee entities:
// 1 Query executed: SELECT * FROM department
List<Department> departments = departmentRepository.findAll();
// When iterating through N departments:
for (Department dept : departments) {
// EACH iteration triggers 1 query: SELECT * FROM employee WHERE department_id = ?
System.out.println(dept.getEmployees().size());
}
If there are 100 departments, Hibernate executes 1 + 100 = 101 database queries! Under load, this saturates DB connection pools and crashes production runtimes.
2. Solution 1: JOIN FETCH in JPQL
JOIN FETCH tells Hibernate to execute an inner or left join and populate the related collection in a single database query:
public interface DepartmentRepository extends JpaRepository<Department, Long> {
@Query("SELECT DISTINCT d FROM Department d LEFT JOIN FETCH d.employees")
List<Department> findAllWithEmployees();
}
Result: Exactly 1 SQL query with an SQL LEFT OUTER JOIN.
3. Solution 2: @EntityGraph
@EntityGraph allows overriding the default fetch plan declaratively without modifying JPQL:
public interface DepartmentRepository extends JpaRepository<Department, Long> {
@EntityGraph(attributePaths = { "employees" })
List<Department> findAll();
}
4. Solution 3: DTO Projections (Zero Overhead)
When querying data solely for read operations (e.g., rendering a table or API response), do not load managed JPA entities into the persistence context. Query directly into lightweight Java Records:
public record DepartmentSummary(Long id, String name, long employeeCount) {}
public interface DepartmentRepository extends JpaRepository<Department, Long> {
@Query("""
SELECT new com.codewithsarvy.dto.DepartmentSummary(d.id, d.name, COUNT(e.id))
FROM Department d LEFT JOIN d.employees e
GROUP BY d.id, d.name
""")
List<DepartmentSummary> findDepartmentSummaries();
}
Benefits of DTO Projections:
- Zero dirty-checking overhead in Hibernate.
- Selects only the required columns from the database (no
SELECT *). - Completely bypasses the N+1 problem.