Получить доступ

Что такое проблема N+1 запросов и как её решать?

MiddleORM и N+1Hibernate / JPASQL
Ещё спрашивают так:
  • Почему Hibernate делает сотни запросов?
  • Как найти проблему N+1?
Коротко

N+1 — один запрос за списком и ещё по запросу на связанные данные каждого элемента: 100 заказов → 101 запрос. Причина — ленивая загрузка связей в цикле.

  • Лечение: JOIN FETCH, @EntityGraph, пакетная загрузка (batch size), DTO-проекции
  • Обнаружение: логи SQL, счётчик запросов в тестах, APM
  • Каждый запрос — сетевой обход: 100 × 1 мс = 100 мс впустую

А ещё JOIN FETCH коллекций с пагинацией — ловушка: Hibernate пагинирует в памяти

Так подсказку видит пользователь sotto на живом собеседовании — за пару секунд, пока вы думаете вслух.

Подробно

Механизм. ORM загружает список сущностей одним запросом, а связи (order.getCustomer(), order.getItems()) помечены ленивыми и подгружаются при первом обращении. Код проходит по списку в цикле — и на каждой итерации идёт отдельный запрос. В шаблоне или сериализаторе JSON это происходит незаметно.

Почему плохо: каждый запрос — обход по сети и разбор, даже быстрый; сотни запросов на страницу дают сотни миллисекунд и нагружают базу.

Как найти:

  • включить лог SQL (spring.jpa.show-sql, p6spy, datasource-proxy) и посмотреть на повторяющиеся запросы;
  • в тестах проверять число запросов (Hypersistence Utils, assertSelectCount);
  • APM-трассировка показывает десятки одинаковых спанов.

Как лечить:

  • JOIN FETCH в JPQL — связь загружается одним запросом с JOIN;
  • @EntityGraph — описать, какие связи загрузить для конкретного запроса;
  • пакетная загрузка — hibernate.default_batch_fetch_size = 50: связи для 50 сущностей подгружаются одним запросом с IN (...);
  • DTO-проекции — выбрать ровно нужные поля одним запросом.

Ловушки: JOIN FETCH двух коллекций — декартово произведение (MultipleBagFetchException); JOIN FETCH коллекции с setMaxResults — Hibernate загрузит всё и пагинирует в памяти (предупреждение HHH000104).

Пример

// N+1: запрос за заказами + по запросу на клиента каждого заказа
for (Order o : orderRepo.findAll()) log.info(o.getCustomer().getName());

// один запрос
@Query("select o from Order o join fetch o.customer")
List<Order> findAllWithCustomer();

Что хотят услышать

  • Механизм через ленивую загрузку в цикле.
  • Как обнаружить.
  • Несколько способов исправления.
  • Ловушки JOIN FETCH — плюс.

Частые ошибки

  • Делать все связи EAGER — N+1 остаётся, а лишние данные грузятся всегда.
  • Не проверять число запросов в тестах.

Что спросят дальше

  • Чем @EntityGraph отличается от JOIN FETCH?
  • Как поймать N+1 в тестах?

Разбор темы

Связанные вопросы

Закрытая бета

Подсказки на собеседовании — в реальном времени

sotto слышит вопрос интервьюера и показывает короткий ответ и план, как в блоке «Коротко». Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем