f in x
JPA Hibernate N+1 Problem — Diagnose and Fix with Real Examples
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

JPA Hibernate N+1 Problem — Diagnose and Fix with Real Examples

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

Your Java app runs fine in development, but in production it takes seconds to load a list of orders. You open the logs and see hundreds of identical SQL queries. You've hit the JPA Hibernate N+1 problem. At Meteora Web, we see it in almost every project that comes to us for a performance audit. It's not a framework flaw — it's improper use of relationships and fetching. In this hands-on guide we'll show you how to recognize, diagnose, and fix it with concrete strategies.

How does the N+1 Problem occur in JPA and Hibernate?

Imagine an Order entity with a @OneToMany relationship to OrderLine. With LAZY fetching (default for collections), when you load an order Hibernate executes one query for the order and, only when you access the lines list, a new query fires for each order. With 100 orders you get 1 + 100 queries. That's the N+1.

Example:

@Entity
public class Order {
    @Id private Long id;
    private String customer;
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderLine> lines = new ArrayList<>();
}
@Entity
public class OrderLine {
    @Id private Long id;
    private String product;
    private int quantity;
    @ManyToOne
    @JoinColumn(name = "order_id")
    private Order order;
}

The code iterating over orders and calculating totals per order:

Sponsored Protocol

List<Order> orders = entityManager.createQuery("SELECT o FROM Order o", Order.class).getResultList();
for (Order o : orders) {
    System.out.println("Total for order " + o.getId() + ": " + o.getLines().stream().mapToInt(OrderLine::getQuantity).sum());
}

Result: 1 query for orders, then 100 queries for lines. The application chokes.

How to diagnose the N+1 Problem in production?

Eye-balling is impossible. You need instrumentation. At Meteora Web we always enable Hibernate statistics in test/staging, and if possible in production with a dedicated logger.

Enable Hibernate statistics

# application.properties
spring.jpa.properties.hibernate.generate_statistics=true
logging.level.org.hibernate.stat=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

In the log you'll see: Number of JDBC connections: 1, PreparedStatement: 101, Time: 1234 ms. If PreparedStatement count is far higher than expected rows, it's N+1.

Sponsored Protocol

Use a custom query counter

You can also implement a simple Spring interceptor to count queries per request:

@Component
public class QueryCounter {
    private final ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> 0);
    
    @Autowired
    private EntityManagerFactory emf;
    
    @PostConstruct
    public void init() {
        SessionFactory sf = emf.unwrap(SessionFactory.class);
        sf.getStatistics().setStatisticsEnabled(true);
    }
    
    public int getQueryCount() {
        return sf.getStatistics().getQueryExecutionCount();
    }
    
    public void reset() {
        sf.getStatistics().clear();
    }
}

Then in a test or debug endpoint call getQueryCount() before and after the operation.

Which fetching strategies fix the N+1?

Hibernate offers several strategies: eager loading, join fetch, batch fetching, entity graph, subselect. Let's see them.

JOIN FETCH in JPQL

The most immediate solution: use JOIN FETCH to load associations in one query.

Sponsored Protocol

List<Order> orders = entityManager.createQuery(
    "SELECT o FROM Order o JOIN FETCH o.lines", Order.class).getResultList();

Works, but beware: with multiple collections you get a cartesian product. Hibernate 6 introduces @Fetch(FetchMode.SUBSELECT) to avoid it.

Batch Fetching

Configure batch fetching at entity level. Hibernate will load LAZY associations in batches.

spring.jpa.properties.hibernate.default_batch_fetch_size=10

Or per mapping:

@OneToMany(mappedBy = "order")
@BatchSize(size = 10)
private List<OrderLine> lines;

With batch size 10, for 100 orders you get 1 + 10 queries (instead of 101).

Subselect

With @Fetch(FetchMode.SUBSELECT), Hibernate runs a second query with a subselect to fetch all collections at once.

@OneToMany(mappedBy = "order")
@Fetch(FetchMode.SUBSELECT)
private List<OrderLine> lines;

Useful when you have many entities and need to load all collections. Caution: if the first query is complex, the subselect can be heavy.

How to use @EntityGraph for dynamic queries?

Spring Data JPA provides @EntityGraph to define fetching paths on repository methods without modifying the model.

Sponsored Protocol

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    @EntityGraph(attributePaths = {"lines"})
    List<Order> findAll();
    
    @EntityGraph(attributePaths = {"lines.product"})
    Optional<Order> findByIdWithLinesAndProducts(Long id);
}

You can also define entity graphs at entity level with @NamedEntityGraph:

@Entity
@NamedEntityGraph(name = "Order.lines", attributeNodes = @NamedAttributeNode("lines"))
public class Order { ... }

Then use it in the repository: @EntityGraph("Order.lines"). This separates concerns: the model stays LAZY, but specific queries load what's needed.

How to integrate batch fetching with Spring Boot?

We already saw the properties. Batch fetching is simple to activate and often enough. But be careful: batch size shouldn't be too high (overhead of bind parameters) nor too low (little improvement). A good starting point is 10–20.

If you use spring.jpa.properties.hibernate.default_batch_fetch_size=10, Hibernate applies batch fetching to all LAZY associations. You can also combine with hibernate.jdbc.batch_size to optimize writes.

Sponsored Protocol

At Meteora Web, we recommend starting with batch fetching before moving to more invasive strategies like JOIN FETCH on every query. It often fixes N+1 without changing a single line of query code.

What to do right now to prevent N+1?

  1. Enable Hibernate statistics in your test and staging environments. Don't wait for production latency reports.
  2. Set a global batch fetch size (hibernate.default_batch_fetch_size=10) as baseline.
  3. Review every repository that runs findAll or queries on collections. Add @EntityGraph where needed.
  4. For multiple collections, use @Fetch(FetchMode.SUBSELECT) or run two separate queries and map in memory.
  5. Monitor query counts with a QueryCounter in integration tests. Write a test that asserts a given operation runs no more than N queries.

The N+1 problem is the silent killer of JPA applications. With these best practices you can keep it under control. If you need a performance audit of your Java application, contact us. We reason about your data, not just the code.

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()