Добавить в корзинуПозвонить
Найти в Дзене
Записки о Java

Проблема N+1 в Hibernate: что это, почему возникает и как с этим бороться

Проблема N+1 — одна из самых распространённых ошибок при работе с ORM, особенно с Hibernate. Она приводит к чрезмерному количеству SQL-запросов к базе данных и, как следствие, к ухудшению производительности. 💡 N+1 означает:1 запрос загружает список сущностей (например, List<Order>),
N запросов (по одному на каждую сущность) загружают связанные данные (например, order.getItems()).
Это особенно критично при большом количестве сущностей (N = 1000 → 1001 запросов!). Рассмотрим простую доменную модель: Теперь представим, что мы хотим вывести все заказы и для каждого — список товаров: ❗ При fetch = FetchType.LAZY (по умолчанию для @OneToMany) связанные сущности подгружаются лениво, то есть только при обращении.
И именно это вызывает N дополнительных запросов — по одному на каждый order.getItems(). Загружаем заказы вместе с товарами одним запросом: DISTINCT нужен, чтобы избежать дублирования заказов при JOIN (если у заказа несколько товаров). Теперь SQL будет примерно таким: JPA предлагает
Оглавление
Рисунок: проблема N+1 в Hibernate
Рисунок: проблема N+1 в Hibernate

Что такое проблема N+1?

Проблема N+1 — одна из самых распространённых ошибок при работе с ORM, особенно с Hibernate. Она приводит к чрезмерному количеству SQL-запросов к базе данных и, как следствие, к ухудшению производительности.

💡 N+1 означает:1 запрос загружает список сущностей (например, List<Order>),
N запросов (по одному на каждую сущность) загружают связанные данные (например, order.getItems()).

Это особенно критично при большом количестве сущностей (N = 1000 → 1001 запросов!).

Пример, иллюстрирующий проблему

Рассмотрим простую доменную модель:

Рисунок: пример entity - Order
Рисунок: пример entity - Order
Рисунок: пример entity OrderItem, часть 1
Рисунок: пример entity OrderItem, часть 1
Рисунок: пример entity OrderItem, часть 2
Рисунок: пример entity OrderItem, часть 2

Теперь представим, что мы хотим вывести все заказы и для каждого — список товаров:

Рисунок: класс OrderDao
Рисунок: класс OrderDao
Рисунок: класс OrderService
Рисунок: класс OrderService

❗ При fetch = FetchType.LAZY (по умолчанию для @OneToMany) связанные сущности подгружаются лениво, то есть только при обращении.
И именно
это вызывает N дополнительных запросов — по одному на каждый order.getItems().

Как обнаружить проблему N+1?

  1. Включите логирование SQL в Hibernate:
Рисунок: включаем логирование в SQL в Hibernate
Рисунок: включаем логирование в SQL в Hibernate

2. Запустите код — вы увидите множество повторяющихся SELECT вроде:

Рисунок: повторяющиеся запросы
Рисунок: повторяющиеся запросы

Способы решения проблемы N+1

Использовать JOIN FETCH в JPQL

Загружаем заказы вместе с товарами одним запросом:

Рисунок: использование JOIN FETCH в SQL
Рисунок: использование JOIN FETCH в SQL
DISTINCT нужен, чтобы избежать дублирования заказов при JOIN (если у заказа несколько товаров).

Теперь SQL будет примерно таким:

Рисунок: SQL запрос при использовании JOIN FETCH
Рисунок: SQL запрос при использовании JOIN FETCH
  • artesian product" при нескольких коллекциях (см. ниже)

Использовать @EntityGraph

JPA предлагает декларативный способ указать, какие ассоциации нужно подгружать:

Рисунок: использование аннотации "NamedEntityGraphAttribute
Рисунок: использование аннотации "NamedEntityGraphAttribute

И в репозитории:

Рисунок: использование аннотации @NamedENtityGraph
Рисунок: использование аннотации @NamedENtityGraph

Использование:

Рисунок: использование EntityGraph в SQL запросе
Рисунок: использование EntityGraph в SQL запросе