Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A Spring Boot endpoint can issue one query for its root entities and then another query for each related entity your code touches. That N+1 pattern is fixed by choosing an explicit, endpoint-specific fetch plan—usually a JPQL/HQL JOIN FETCH or Spring Data JPA @EntityGraph—then verifying the emitted SQL and measuring the endpoint with realistic data.

What the N+1 query problem actually is

N+1 is a query pattern, not a single Hibernate error. The application first loads N root rows, then traverses an association and causes additional selects. If an endpoint loads 50 orders and accesses each order’s customer, Hibernate can execute one query for the orders plus 50 customer queries: 51 statements in total.

A typical service method looks harmless:

List<Order> orders = orderRepository.findByStatus(Status.OPEN);
return orders.stream()
    .map(order -> new OrderResponse(
        order.getId(),
        order.getCustomer().getName()))
    .toList();

The repository method loads the orders. The call to getCustomer() traverses the association for every order. With a lazy relationship, each traversal can initialize a proxy and issue a select. Hibernate’s user guide describes the same failure mode when an association is omitted from a JPQL query: a secondary select may be executed for each association, producing N+1 behavior. See the Hibernate ORM User Guide.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The exact trace depends on the mapping, query shape, Hibernate version, session and second-level cache state, and which properties the response serializer or business code reads. An eager mapping does not guarantee one SQL statement either. Hibernate warns that an EAGER association omitted from a JPQL query can still be fetched with secondary selects. Changing every relationship to eager loading therefore replaces one source of surprises with another.

What the SQL often looks like

For an order-to-customer relationship, the log may contain:

select o.id, o.customer_id, o.status
from orders o
where o.status = ?;

select c.id, c.name
from customers c
where c.id = ?;

select c.id, c.name
from customers c
where c.id = ?;

The repeated statements have the same structure and differ only in the bound foreign-key value. The root query is the “1”; the repeated association selects are the “N”. A database may reuse its execution plan, but it still receives many round trips and performs repeated work.

Confirm that the endpoint, not a different code path, is generating N+1

Diagnose the SQL emitted by the actual HTTP request. Repository method names and entity annotations are clues; they do not prove what Hibernate sent to the database.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Reproduce one request in a controlled environment. Use a development or approved diagnostic environment with representative authentication, filters, and serialization. Record the endpoint, request parameters, database, Spring Boot version, Hibernate version, and dataset size.
  2. Enable provider-approved SQL logging for that environment. JPA does not standardize JPQL or SQL logging, so Spring Data JPA documents provider-specific configuration rather than one universal switch. Follow the configuration for the Hibernate version deployed by your application. Avoid leaving verbose SQL and bind-value logging enabled in production without reviewing performance, log volume, and possible data exposure.
  3. Optionally enable Hibernate SQL comments. Spring Boot can pass spring.jpa.properties.hibernate.use_sql_comments=true, which adds Hibernate comments to generated SQL. This setting annotates statements; it is not, by itself, SQL statement logging. The relevant Spring Data JPA guidance is in JPA Query Methods.
  4. Count statements and inspect their shape. Look for one root select followed by repeated selects whose predicates differ only by an identifier. Include statements issued during JSON serialization, not just those inside the repository call.
  5. Vary the number of roots. Run the same endpoint with a small result and then a substantially larger result. If the number of association statements grows with the number of root entities, lazy traversal or an omitted eager association is a strong suspect. A fixed number of statements can indicate a different problem, such as a single oversized join or an unrelated query in middleware.
  6. Check caches and session boundaries. A first-level session cache, second-level cache, test fixtures, or a warm database can hide repeated loads. Repeat the test under the same cache conditions you intend to compare.

Choose a fetch plan for this response

Fetch strategy should follow the endpoint’s data contract. Hibernate recommends lazy mappings for most associations and explicit fetching when a use case needs related data. The objective is not “the fewest possible queries” in isolation; it is an appropriate balance of round trips, rows, transferred bytes, memory, pagination behavior, and response latency.

Approach Typical SQL shape When it fits Main trade-offs
JPQL/HQL JOIN FETCH One statement joining the requested association The endpoint always needs a known relationship and the joined result remains manageable Joining large or multiple collections can multiply result rows and transfer redundant parent data; pagination and collection behavior must be checked for the exact query and provider version
Spring Data JPA @EntityGraph Provider-generated fetch plan, commonly a join or equivalent strategy You want a repository method to declare which attributes are needed without embedding a long JPQL string The generated SQL remains provider-dependent, so inspect it; a graph is not a guarantee of one statement
DTO or projection query Select only columns required by the response The endpoint is read-only and does not need managed entities or broad graph traversal More query-specific code; changing the response may require changing the projection
Batch fetching Separate association selects grouped with an IN restriction A join would create an excessive result set, but loading several related entities together is still useful It mitigates repeated selects rather than eliminating the underlying pattern; batch size and access order affect results

Hibernate’s documentation calls outer-join fetching “usually the best way to fetch associations” while also recognizing cases where large result sets make another strategy preferable. Its older fetching guide explains SELECT, JOIN, and BATCH strategies; consult the documentation matching your deployed version at Hibernate ORM 5.0 Fetching. Neither guide establishes a universally fastest choice for every schema.

Implement a scoped fix

Use a fetch join when the relationship is always needed

Put the fetch decision on the query used by the endpoint, rather than changing the entity globally:

public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("""
        select o
        from Order o
        join fetch o.customer
        where o.status = :status
        """)
    List<Order> findOpenOrdersWithCustomer(@Param("status") Status status);
}

The join fetch tells Hibernate that the customer is part of this load plan. Keep the selected associations limited to what the response actually reads. If you join a collection, inspect the returned row count: a parent with many children can appear in multiple SQL rows, and joining more than one large collection can multiply those rows again.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Declare an EntityGraph on a Spring Data method

Spring Data JPA supports @EntityGraph on repository query methods. This keeps the fetch plan close to a derived or annotated query:

public interface OrderRepository extends JpaRepository<Order, Long> {
    @EntityGraph(attributePaths = "customer")
    List<Order> findByStatus(Status status);
}

The graph expresses the attributes needed by this method; Hibernate still chooses the concrete SQL strategy. Verify that choice in the log and test it with the deployed Spring Data, Hibernate, and database versions.

Return a DTO when entities are unnecessary

A read endpoint can select exactly the fields it serializes:

public record OrderSummary(Long id, String customerName) {}

@Query("""
    select new com.example.api.OrderSummary(o.id, c.name)
    from Order o
    join o.customer c
    where o.status = :status
    """)
List<OrderSummary> findSummaries(@Param("status") Status status);

This avoids exposing a managed entity graph to later code that might accidentally traverse more associations. It also makes the response’s columns explicit, although the query must be maintained as the response changes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use batching when a join would be too wide

Batch fetching groups several pending association loads into a separate select using an IN restriction. That can be a practical compromise when a large join would produce too many duplicate parent rows. Treat it as mitigation: it reduces the number of round trips but does not make an implicit traversal an explicit endpoint contract. Configure and size batching according to the Hibernate version and database limits, then verify the resulting statements rather than assuming a particular batch size is optimal.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Why common “fixes” fail

Changing every relationship to EAGER

Global eager loading can pull data into endpoints that do not need it, create unexpected joins or secondary selects, and enlarge memory use. Hibernate specifically notes that an eager association omitted from a JPQL query can still trigger secondary selects. Keep mappings generally lazy and define fetch plans per use case.

Adding every association to one fetch join

A single giant query may remove repeated selects while returning a Cartesian multiplication of rows for multiple collections. The database and application then process far more data than the JSON response needs. Add one required association at a time and compare rows and bytes, not only statement count.

Assuming fewer statements means a faster endpoint

A joined query can consume more database CPU, sort or hash larger intermediate results, and transfer duplicate parent columns. Conversely, several small batched selects may outperform a very wide join for a particular dataset. Query count is a diagnostic signal, not a latency result.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fixing only the repository call

Serialization, mapping code, authorization checks, and template or projection logic can traverse additional relationships after the repository returns. Capture the complete request and inspect every statement until the response is written.

Validate the change with representative measurements

Apply one fetch-plan change at a time so that the SQL difference is attributable. For each version of the endpoint, record:

  • Spring Boot, Spring Data JPA, Hibernate, database, and driver versions.
  • Number of root rows, related rows, and collection sizes.
  • Total SQL statements, database round trips, result-row count, and approximate bytes transferred.
  • Endpoint latency at an agreed percentile, plus database CPU, waits, and connection-pool usage where available.
  • Pagination parameters and whether the request is a cold or warm cache run.

Compare a small dataset and a production-shaped dataset. A change that looks excellent with five orders may fail with thousands. If the endpoint is paginated, test the exact page size and ordering; collection joins can alter row multiplicity and require provider-specific handling. Validate returned records as well as performance so that a reduced query count has not silently dropped or duplicated data.

Do not report a latency or throughput improvement without stating the environment, data volume, database and ORM versions, measurement method, and metric. The Hibernate and Spring Data documentation explains the mechanisms and trade-offs, but it does not establish a universal percentage improvement or a production result for your application.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A practical rollout checklist

  • Identify the endpoint and the association accessed during response construction.
  • Capture its actual SQL in a controlled diagnostic environment.
  • Confirm whether statement count grows with the number of root entities.
  • Choose one fetch join, EntityGraph, DTO query, or batch strategy for that response.
  • Inspect SQL, row multiplicity, returned data, and pagination behavior.
  • Measure latency and database work with representative data.
  • Review logs for sensitive values and disable verbose diagnostics when the investigation ends.
  • Document why the selected fetch plan belongs to this endpoint rather than making it a global mapping default.

How to decide between the options

Use a fetch join when the endpoint needs one modest relationship and a single joined result remains predictable. Prefer an EntityGraph when the repository query is already suitable and you want a readable, method-scoped fetch declaration. Use a DTO query when the response needs a narrow, stable column set and no entity behavior. Consider batching when joining would produce an unwieldy result set but related objects still need to be loaded together. In every case, let the emitted SQL and measured endpoint behavior decide; there is no provider-independent rule that one strategy wins for all schemas.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.