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:
JOIN FETCHEntityGraphBatchSizeSubselect
- 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
clientsandpaymentstables are related viaclient_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