Solving the N+1 Select Problem in Hibernate: Best Practices and Strategies

Illustration for Solving the N+1 Select Problem in Hibernate: Best Practices and Strategies
By Last updated:

Hibernate is a powerful ORM (Object Relational Mapping) tool that simplifies database interaction in Java applications. But one of the most common performance issues developers face is the N+1 Select Problem. Left unchecked, this issue can cripple your application’s performance by executing dozens or even hundreds of unnecessary queries.

Think of it like ordering food at a restaurant: instead of placing one consolidated order, you keep calling the waiter back for every single item. That’s the N+1 problem.

In this tutorial, we’ll explore what the N+1 problem is, why it happens, and how to solve it effectively in Hibernate.


What is the N+1 Select Problem?

The N+1 problem occurs when Hibernate executes 1 query to fetch a parent entity and then N additional queries to fetch each associated child entity.

Example Scenario

Suppose you have Department and Employee entities with a One-to-Many relationship:

@Entity
@Table(name = "departments")
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
    private List<Employee> employees = new ArrayList<>();
}

@Entity
@Table(name = "employees")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;
}

Now, fetching all departments and their employees:

List<Department> departments = session.createQuery("FROM Department", Department.class).list();

for (Department dept : departments) {
    for (Employee emp : dept.getEmployees()) {
        System.out.println(emp.getName());
    }
}

SQL Generated

  • 1 query to fetch all departments.
  • N queries to fetch employees for each department.

This is the N+1 problem.


Why Does the N+1 Problem Happen?

By default, associations like @OneToMany and @ManyToOne are often lazily loaded. Hibernate doesn’t fetch related entities until they are accessed, causing extra queries.

While lazy loading prevents loading unnecessary data, it can cause too many small queries, reducing performance.


Strategies to Solve the N+1 Select Problem

1. Fetch Joins with HQL/JPQL

The simplest way to solve N+1 is to fetch associations in a single query using JOIN FETCH.

List<Department> departments = session.createQuery(
    "SELECT DISTINCT d FROM Department d JOIN FETCH d.employees",
    Department.class).list();

Generated SQL: One query fetching both departments and employees.

⚠ Use DISTINCT to avoid duplicate parent entities.


2. @BatchSize Annotation

@BatchSize allows batching of lazy-loaded associations, reducing the number of queries.

@Entity
@Table(name = "departments")
@BatchSize(size = 10)
public class Department {
    // ...
}

If you load 50 departments, Hibernate will fetch employees in batches of 10 instead of 50 separate queries.


3. Global Batch Fetching

You can configure a global batch fetch size in hibernate.properties:

hibernate.default_batch_fetch_size=20

This fetches lazy-loaded collections in batches, improving performance.


4. Entity Graphs

Entity graphs let you define fetch plans dynamically.

EntityGraph<Department> graph = entityManager.createEntityGraph(Department.class);
graph.addSubgraph("employees");

List<Department> departments = entityManager
    .createQuery("SELECT d FROM Department d", Department.class)
    .setHint("javax.persistence.fetchgraph", graph)
    .getResultList();

This tells Hibernate to fetch employees eagerly only when required.


5. Second-Level Cache

Hibernate’s second-level cache (e.g., Ehcache, Infinispan) can reduce redundant queries by caching associated entities.

hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory

⚠ Caching should complement other strategies, not replace them.


6. Avoiding Eager Fetching Everywhere

While eager fetching can fix N+1, overusing it may lead to loading massive datasets unnecessarily. Use eager fetching only when relationships are always required.


Real-World Use Case: Spring Boot + Hibernate

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 30
    hibernate:
      ddl-auto: update

Now, repositories can handle batch fetching automatically:

public interface DepartmentRepository extends JpaRepository<Department, Long> {
    @EntityGraph(attributePaths = "employees")
    List<Department> findAll();
}

This avoids N+1 while keeping queries clean.


Common Pitfalls & Anti-Patterns

  1. Using Eager Fetching Everywhere → Leads to huge joins and memory issues.
  2. Ignoring Batch Size Configurations → Defaults may still cause N+1.
  3. Mixing Fetch Strategies Randomly → Causes unexpected query plans.
  4. Caching Without Strategy → Might mask N+1 instead of solving it.

Best Practices

  • Start with Lazy Loading + Fetch Joins when needed.
  • Use @BatchSize and hibernate.default_batch_fetch_size for collections.
  • Apply Entity Graphs for fine-grained control.
  • Always monitor SQL logs to detect N+1 queries early.
  • Test under production-like datasets for realistic performance results.

📌 Hibernate Version Notes

Hibernate 5.x

  • Uses javax.persistence namespace.
  • Fetch joins and batch fetching supported.
  • Entity graphs via JPA 2.1.

Hibernate 6.x

  • Migrated to jakarta.persistence namespace.
  • Improved SQL generation and query planning.
  • More powerful support for dynamic fetch strategies.
  • Better alignment with JPA specifications.

Conclusion & Key Takeaways

  • The N+1 Select Problem can devastate Hibernate performance.
  • Use fetch joins, @BatchSize, entity graphs, and caching wisely.
  • Avoid blanket eager fetching—fetch only what you need, when you need it.
  • Regularly profile queries to ensure optimal performance.

FAQ: Expert-Level Questions

Q1: What’s the difference between Hibernate and JPA?
Hibernate is a JPA implementation with additional features like caching and batch fetching.

Q2: How does Hibernate caching improve performance?
It reduces repetitive queries by storing frequently accessed entities in memory.

Q3: What are the drawbacks of eager fetching?
It loads unnecessary data, leading to slower queries and memory overhead.

Q4: How do I solve the N+1 select problem in Hibernate?
Use fetch joins, @BatchSize, entity graphs, or batch fetch settings.

Q5: Can I use Hibernate without Spring?
Yes, you can use standalone Hibernate with SessionFactory.

Q6: What’s the best strategy for inheritance mapping?
Depends: SINGLE_TABLE for performance, JOINED for normalization, TABLE_PER_CLASS for isolation.

Q7: How does Hibernate handle composite keys?
By using @Embeddable and @EmbeddedId or @IdClass.

Q8: How is Hibernate 6 different from Hibernate 5?
Hibernate 6 moves to jakarta.persistence, improves query APIs, and enhances SQL support.

Q9: Is Hibernate suitable for microservices?
Yes, but for smaller services, lightweight alternatives like MyBatis or jOOQ may be better.

Q10: When should I not use Hibernate?
Avoid Hibernate when raw SQL performance or fine-grained query control is critical (e.g., analytics-heavy apps).