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)