What Is the N+1 Problem?
The N+1 SELECT problem happens when JPA (or Hibernate) executes one query to fetch a list of entities, and then N additional queries — one for each entity — to load their related data.
It’s called “N+1” because you get:
- 1 query to fetch the parent entities
- N queries to fetch each child entity or lazy association
This usually happens because of lazy loading (the default in JPA for @OneToMany
and @ManyToOne
relationships).
Example Scenario
Let’s imagine we have a simple blog model:
-
Author
(id, name) -
Post
(id, title, content, author)
An Author has many Posts.
Entity Setup
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Post> posts = new ArrayList<>();
// getters, setters
}
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
// getters, setters
}
Repository Layer
public interface AuthorRepository extends JpaRepository<Author, Long> {
}
Step 1: Reproduce the N+1 Problem
In your service or controller:
@Service
@Transactional(readOnly = true)
public class AuthorService {
private final AuthorRepository authorRepository;
public AuthorService(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
public void printAuthorsAndPosts() {
List<Author> authors = authorRepository.findAll();
for (Author author : authors) {
System.out.println("Author: " + author.getName());
for (Post post : author.getPosts()) {
System.out.println(" Post: " + post.getTitle());
}
}
}
}
Step 2: Observe the SQL Queries
Enable SQL logging in application.properties
:
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
You’ll see something like this:
select a.id, a.name from author a;
select p.id, p.title, p.content, p.author_id from post p where p.author_id = 1;
select p.id, p.title, p.content, p.author_id from post p where p.author_id = 2;
select p.id, p.title, p.content, p.author_id from post p where p.author_id = 3;
...
That’s 1 query for authors + N queries for posts — the N+1 problem.
Why It’s a Problem
- Causes massive performance issues when data grows.
- Increases database round trips.
- Makes your application slower and more resource-heavy.
Step 3: Fixing the N+1 Problem
There are multiple solutions depending on the situation.
Option 1: Use JOIN FETCH
in JPQL
You can customize your query to load the posts together with authors:
@Query("SELECT a FROM Author a JOIN FETCH a.posts")
List<Author> findAllWithPosts();
Now in your service:
List<Author> authors = authorRepository.findAllWithPosts();
for (Author author : authors) {
System.out.println(author.getName() + " has posts: " + author.getPosts().size());
}
Generated SQL:
select a.*, p.*
from author a
join post p on a.id = p.author_id;
Only one query — no N+1 issue.
Option 2: Use @EntityGraph
JPA provides a declarative way to define which associations to fetch eagerly:
@EntityGraph(attributePaths = "posts")
@Query("SELECT a FROM Author a")
List<Author> findAllWithPosts();
Or simpler:
@EntityGraph(attributePaths = "posts")
List<Author> findAll();
This tells Hibernate to fetch posts
together in one query.
Option 3: Use Batch Fetching (Hibernate Optimization)
If you often need to load collections lazily but want to avoid N+1, enable batch fetching in your application.properties
:
spring.jpa.properties.hibernate.default_batch_fetch_size=10
This groups lazy loads into batches:
Instead of:
SELECT * FROM post WHERE author_id = 1;
SELECT * FROM post WHERE author_id = 2;
SELECT * FROM post WHERE author_id = 3;
You’ll get:
SELECT * FROM post WHERE author_id IN (1,2,3);
Reduced queries, still lazy loading.
Step 4: Benchmark Difference
Scenario | Queries | Notes |
---|---|---|
Default (Lazy Loading) | 1 + N | Slowest |
JOIN FETCH | 1 | Fastest, eager |
EntityGraph | 1 | Declarative, flexible |
Batch Fetching | ~1 + N/k | Balanced approach |
Summary
Problem | Cause | Fix |
---|---|---|
N+1 SELECT | Lazy loading of relationships | Use JOIN FETCH , @EntityGraph , or batch fetching |
When to use JOIN FETCH
|
When you always need the association | |
When to use EntityGraph
|
When you sometimes need the association | |
When to use Batch Fetching | When you prefer lazy loading but want fewer SQL queries |
Complete Example Repository
You can structure your project like this:
src/
├── main/java/com/example/nplus1/
│ ├── entity/
│ │ ├── Author.java
│ │ └── Post.java
│ ├── repository/
│ │ └── AuthorRepository.java
│ ├── service/
│ │ └── AuthorService.java
│ └── NPlus1Application.java
└── main/resources/
└── application.properties
Final Thoughts
- The N+1 problem is subtle and can go unnoticed in small datasets.
- Always check your SQL logs or use Hibernate Statistics or p6spy to detect it.
- Choose a strategy (
JOIN FETCH
,EntityGraph
, or batch fetching) that best fits your data access pattern.
Top comments (0)