f in x
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

JPA Hibernate N+1 Problem — Best Practices to Eliminate It and Achieve 10x Faster Queries

[2026-06-22] Author: Ing. Calogero Bono

Have you ever noticed that a simple list of entities in JPA takes seconds to load, and after enabling SQL logs you see dozens of identical queries executed for every parent row? Welcome to the N+1 problem — the most subtle and common performance killer in any Java application using Hibernate. We at Meteora Web have seen it in client projects where developers believed they wrote clean code — but were unknowingly paying the query bill.

This guide shows you where N+1 comes from, how to diagnose it with concrete tools, and most importantly how to fix it with the best practices we use in our Laravel and Spring Boot projects. Because an ORM is not magic: if you don't understand it, it will cost you performance and money.

What Causes the N+1 Problem in JPA and Hibernate

N+1 occurs when, to retrieve N child entities associated with a parent entity, Hibernate first executes one query to fetch the list of parents, and then N separate queries for each child. The result? Response times that grow linearly with data — and on real volumes become unmanageable.

Concrete example: Suppose we have Author and Book.

@Entity
public class Author {
    @Id
    private Long id;
    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books;
}

@Entity
public class Book {
    @Id
    private Long id;
    private String title;

    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;
}

Now, when you call author.getBooks() for each author, Hibernate executes a query per author. With 100 authors you get 1 (list authors) + 100 (books per author) = 101 queries.

Sponsored Protocol

How to Diagnose N+1 with SQL Logs

Enable query 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.type.descriptor.sql.BasicBinder=TRACE

If you see dozens of identical SELECTs for the same table after the main query, you have N+1. You can also use Hibernate Statistics or the JPA Buddy plugin in IntelliJ to count queries.

How to Fix N+1 Using Fetch Join in JPQL

The most direct solution is to use a fetch join in the JPQL query. Instead of lazy loading, you load everything in a single query with JOIN FETCH.

@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();

This query executes a single SELECT with an SQL join. Hibernate loads authors and books in one go. Zero extra queries. Be careful: if you need to avoid duplicates (when an author has many books), add DISTINCT:

Sponsored Protocol

@Query("SELECT DISTINCT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();

When Not to Use Fetch Join

If you have more than one collection to load (e.g., books and articles), a single multiple join can cause a Cartesian product. In that case, prefer @EntityGraph or multiple queries with batch fetching.

What Is the Difference Between FetchType.LAZY and EAGER for N+1

FetchType.LAZY (default for @OneToMany) is the right choice: it loads children only when actually accessed. But if you access all children for every parent, N+1 kicks in.

FetchType.EAGER always loads everything, even when not needed. It doesn't solve N+1: it causes the same problem but always, even for pages that don't need children. Moreover, with EAGER on multiple relationships, Hibernate may execute extra queries or multiple joins that degrade performance.

Sponsored Protocol

The rule we follow: always use LAZY as default, then optimize with fetch join or @EntityGraph where needed.

How to Use @EntityGraph to Solve N+1 Without Writing JPQL

@EntityGraph is a Spring Data JPA annotation that lets you specify which associations to load eagerly for a specific query, without changing the global fetch type.

@EntityGraph(attributePaths = {"books"})
@Query("SELECT a FROM Author a")
List<Author> findAllWithBooks();

Spring Data JPA automatically generates a fetch join for the specified relationships. Less code, same result. It also works with derived query methods:

@EntityGraph(attributePaths = {"books"})
List<Author> findByNameContaining(String name);

Caution: attributePaths can include nested paths, e.g., "books.publisher". But avoid loading too many relationships in a single graph — you risk the Cartesian product again.

Batch Fetching: An Elegant Alternative to Reduce N+1 Without Changing Queries

If you cannot modify every query (e.g., using a generic repository), configure batch fetching. Hibernate collects primary keys of multiple entities into a single WHERE IN clause, reducing queries from N to N/batch.

Sponsored Protocol

In application.properties:

spring.jpa.properties.hibernate.default_batch_fetch_size=10

Or at entity level with @BatchSize:

@Entity
@BatchSize(size = 10)
public class Author {
    // ...
}

With batch size 10, 100 authors generate 10 queries instead of 100. Not perfect like a single join, but a good compromise when you don't control the queries.

How to Avoid N+1 with Named Entity Graphs and Subclasses

For advanced use, Spring Data JPA supports @NamedEntityGraph at entity level. Define a reusable graph and apply it in multiple repositories.

@Entity
@NamedEntityGraph(name = "Author.books", attributeNodes = @NamedAttributeNode("books"))
public class Author {
    // ...
}

// Repository
@EntityGraph("Author.books")
List<Author> findAll();

This technique centralizes loading logic and makes it more maintainable.

Tools to Detect N+1 in Production

Beyond logs, these tools can save your life:

  • Hibernate Statistics: enable with spring.jpa.properties.hibernate.generate_statistics=true. Shows query count, average time, and flushes.
  • JPA Inspector (IntelliJ plugin): highlights slow queries during development.
  • Hypersistence Optimizer: analyzes code and suggests fixes.
  • Spring Boot Actuator with Hibernate metrics.

We always use statistics in our staging environments before going to production. An app that loads 10 rows locally won't show the problem — with 10,000 rows it becomes obvious.

Sponsored Protocol

Best Practices for ORM Performance with JPA and Hibernate

  1. Never use FetchType.EAGER on collection associations. Prefer LAZY and optimize at the point of use.
  2. Use JOIN FETCH or @EntityGraph for every query that needs to load children.
  3. Set a reasonable default_batch_fetch_size (10-20) to cover unoptimized cases.
  4. Log queries in development and use statistics in staging.
  5. Avoid loading two different collections in a single query — use @BatchSize or separate queries.

Remember: an ORM is not a magic wand. It's a powerful automation that needs to be controlled. If you let it loose, it will fill your logs with queries. If you guide it, it delivers performance comparable to hand-written SQL but with the flexibility of objects.

Want to dive deeper into performance management in a Spring Boot application? Check out our Java and JVM Modern pillar article.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()