Логотип Workflow

Article

Updated at:

N Plus One Problem

Этап 9 - N+1 Problem: поиск и исправление лишних запросов

N+1 означает один query для основного списка плюс один дополнительный relation query для каждого элемента списка. Если один query загрузил 100 заказов, а затем Hibernate выполнил еще 100 queries для загрузки пользователей, приложение выполнило 1 + 100 queries. Код может выглядеть чисто, но работа с базой будет неэффективной.

N plus one query problem

Почему возникает N+1

N+1 часто возникает из-за lazy loading. Lazy loading не является ошибкой; он не дает загружать лишние данные заранее. Проблема появляется, когда код проходит по многим entities и трогает lazy relation у каждой entity.

List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
    System.out.println(order.getUser().getEmail());
}

Первая строка загружает orders. Цикл трогает user у каждого order. Если users еще не были загружены, Hibernate отправляет дополнительные selects. На маленькой базе это легко не заметить. На реальных данных и с network latency это становится performance issue.

Как увидеть проблему

На время обучения включи SQL logging:

spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG

Затем выполни endpoint и посчитай queries. Нужный pattern такой: один select для основного списка, затем много похожих selects для relation. Не останавливайся на факте "endpoint работает". Persistence performance нужно проверять по реальному SQL.

Способы решения

JOIN FETCH говорит JPQL загрузить relation в том же query для конкретного use case:

@Query("SELECT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUsers();

@EntityGraph позволяет описать, какие relations нужно fetch-ить для repository method, без полного JPQL:

@EntityGraph(attributePaths = "user")
List<Order> findAll();

DTO projection может быть еще лучше для read-only API, потому что загружает ровно те fields, которые нужны response. Batch fetching уменьшает количество queries, загружая связанные строки группами. В Hibernate настройка hibernate.default_batch_fetch_size=20 может превратить много одиночных relation queries в меньшее число batched queries.

Что видно в SQL logsЧто это означаетЛучшее первое исправление
select * from orders, затем много select * from users where id = ?Код загрузил orders, потом в цикле тронул order.user.Добавить use-case query с JOIN FETCH o.user или DTO projection.
Одному endpoint нужны users, другому нетMapping не должен становиться globally eager ради одного экрана.Оставить relation lazy и создать fetch plan только для нужного endpoint.
List response требует только order id, status и user emailFull entities грузят больше данных, чем нужно response.Использовать DTO projection и выбрать ровно эти fields.
Много lazy relations читаются маленькими группамиExtra queries остаются, но их можно группировать.Настроить batch fetching как mitigation, а не как единственный design.

Компромиссы

Не стоит решать каждый N+1 переводом mappings в eager. Это распространяет стоимость на каждый query. JOIN FETCH может дублировать rows при fetch collections и требует аккуратности с DISTINCT и pagination. Entity graphs выглядят чисто, но все равно требуют понимания, что загружается. DTOs быстрые для чтения, но не являются managed entities. Batch fetching помогает, но не заменяет use-case-specific queries.

Практика

Создай orders, связанные с users. Загрузи все orders и выведи user emails при включенном SQL logging. Посчитай queries и подтверди pattern N+1. Затем исправь repository через JOIN FETCH и посчитай снова. Повтори через @EntityGraph. В конце создай DTO query, который возвращает order id, status и user email, и сравни SQL и объем загруженных данных.

Чек-лист понимания

  • Могу объяснить, почему N+1 означает один главный query плюс N relation queries.
  • Знаю, как lazy loading может создать N+1.
  • Могу найти N+1 по Hibernate SQL logs.
  • Могу исправить N+1 через JOIN FETCH, @EntityGraph, DTO projection или batch fetching.
  • Понимаю, почему global EAGER loading обычно не является правильным исправлением.

Авторизуйтесь чтоб пройти тесты

Practice

Интерактивная практика

Выполните задания и сразу проверьте ответ.