The N+1 Problem in Hibernate: Complete Guide and Solutions

Aug 14, 2024

The N+1 Problem in Hibernate: Complete Guide and Solutions

The N+1 problem is one of the most common performance pitfalls when working with ORM frameworks like Hibernate. It’s a frequent interview topic for Java developers because it directly affects application efficiency and database load.

In this article, we’ll cover:

  • What the N+1 problem is
  • Why it happens in Hibernate
  • How to fix it with four main approaches:
    1. JOIN FETCH
    2. EntityGraph
    3. BatchSize
    4. Subselect
  • Pros and cons of each method

🧩 What Is the N+1 Problem?

Imagine two entities — Client and Payment — linked by a @OneToMany relationship:

  • One client can have many payments.
  • The clients and payments tables are related via client_id.

When we fetch all clients, Hibernate executes one SQL query:

SELECT * FROM clients;

However, when we access each client’s payments, Hibernate executes an additional query per client:

SELECT * FROM payments WHERE client_id = ?;

If you have 50 clients, Hibernate will execute 51 queries. If you have 1000 clients — 1001 queries. That’s the N+1 query problem.

##⚙️ Why Hibernate Does This

By default, relationships in Hibernate use lazy loading (fetch = FetchType.LAZY). That means related entities are loaded only when accessed, not upfront.

Hibernate can’t know whether you actually need to access the payments for each client — so it loads them separately on demand.

##🔧 How to Fix the N+1 Problem ###1. JOIN FETCH

The most straightforward way to fix N+1 is to explicitly tell Hibernate to perform a JOIN in the query.

Using JPQL:

@Query("SELECT c FROM Client c JOIN FETCH c.payments")
List<Client> findAllWithPayments();

Or directly via EntityManager:

String jpql = "SELECT c FROM Client c JOIN FETCH c.payments";
List<Client> clients = entityManager
    .createQuery(jpql, Client.class)
    .getResultList();

How it works: Hibernate performs a single SQL query with a JOIN between clients and payments.

✅ Pros:

Simple and effective

Executes one query

⚠️ Cons:

Complex when joining multiple collections

Can cause data duplication if misused

###2. EntityGraph

EntityGraph allows you to declaratively specify which relationships should be fetched eagerly. It’s a flexible, annotation-based alternative to JOIN FETCH.

Dynamic EntityGraph
EntityGraph<Client> graph = entityManager.createEntityGraph(Client.class);
graph.addSubgraph("payments");

List<Client> clients = entityManager
    .createQuery("SELECT c FROM Client c", Client.class)
    .setHint("javax.persistence.fetchgraph", graph)
    .getResultList();

Named EntityGraph
@NamedEntityGraph(
    name = "Client.withPayments",
    attributeNodes = @NamedAttributeNode("payments")
)
@Entity
public class Client { ... }

Usage:

EntityGraph<?> graph = entityManager.getEntityGraph("Client.withPayments");
List<Client> clients = entityManager
    .createQuery("SELECT c FROM Client c", Client.class)
    .setHint("javax.persistence.fetchgraph", graph)
    .getResultList();

With Spring Data JPA

@EntityGraph(attributePaths = {"payments"})
@Query("SELECT c FROM Client c")
List<Client> findAllWithPayments();

✅ Pros:

Highly flexible

Easy to reuse across queries

⚠️ Cons:

More verbose than JOIN FETCH

Overkill for simple queries

###3. BatchSize

If you prefer to keep lazy loading but reduce the number of queries, you can use batch fetching.

Global configuration: spring.jpa.properties.hibernate.default_batch_fetch_size=16

Or per-entity:

@BatchSize(size = 10)
@OneToMany(mappedBy = "client", fetch = FetchType.LAZY)
private List<Payment> payments;

How it works: Hibernate still loads related entities lazily, but in batches rather than one by one. If you have batch_size = 16 and 50 clients, Hibernate will execute:

1 query for clients

4 queries for payments (fetching 16 clients’ payments per query)

✅ Pros:

Retains lazy loading

Greatly reduces query count

⚠️ Cons:

Still multiple queries

Requires tuning batch size

###4. Subselect

Another elegant approach is to use @Fetch(FetchMode.SUBSELECT).

@OneToMany(mappedBy = "client", fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
private List<Payment> payments;

How it works: Hibernate will execute:

One query for clients

One subselect query for all related payments:

SELECT * FROM payments
WHERE client_id IN (SELECT id FROM clients);

✅ Pros:

Only two queries total

No data duplication like with joins

⚠️ Cons:

Doesn’t work well with pagination (LIMIT / OFFSET)

Can have unpredictable performance for very large datasets

📊 Comparison Table

Approach Query Count Flexibility Complexity Best Use Case JOIN FETCH 1 Medium Simple Simple eager loading EntityGraph 1 High Medium Configurable loading strategies BatchSize ~N/batch High Simple Optimize lazy loading Subselect 2 Low Simple Mass loading without JOIN

🧾 Summary

The N+1 problem isn’t a bug — it’s a side effect of lazy loading in Hibernate. But you should be aware of it and choose the right loading strategy depending on your use case.

In short:

For simple eager loading → JOIN FETCH

For flexible control → EntityGraph

For optimizing lazy loading → BatchSize

For mass loading without joins → Subselect

ilia-kritiuk.dev