Проблема N+1 — это классический «тихий убийца» производительности, на котором горят даже опытные разработчики. Мы привыкли доверять ORM, полагая, что под капотом всё уже оптимизировано за нас. Кажется, что достаточно вызвать метод и работать с данными как с обычными объектами языка. Но ORM — это всего лишь инструмент, и если использовать его вслепую, база данных ляжет быстрее, чем вы закончите код-ревью.
Механика деградации
Представим стандартную структуру: коллекция блюд, у каждого есть связь (relation) с категорией. Задача — отрисовать список блюд и для каждого вывести название его категории. Типичный сценарий: вы отправляете один запрос, загружаете продукты и начинаете перебирать их в цикле. Внутри цикла вы обращаетесь к свойству:
product.category.В коде это выглядит как обычный доступ к полю объекта. Но по факту данных там еще нет, потому что при первом запросе вы вытащили только продукты. Чтобы отдать вам категорию, ORM вынуждена «бегать» в базу на каждой итерации цикла.
Математика здесь беспощадна. У вас получается
N + 1 запрос:- 1 — первичный запрос на получение списка продуктов.
- N — количество дополнительных запросов, где N равно числу продуктов.
Если в списке 100 позиций — вы делаете 101 запрос. Если 1000 — 1001. Чем больше данных, тем сильнее вы нагружаете бэкэнд и СУБД инфраструктурным перегрузом: подготовкой SQL, установкой соединений и ожиданием ответов.
Логика «пустой банки»
Это похоже на уборку в квартире. Вы нашли пустую банку из-под колы, пачку от чипсов и какой-то фантик. Вместо того чтобы собрать весь мусор в один пакет и вынести его за раз, вы берете банку, идете к мусоропроводу, возвращаетесь, берете пачку, снова идете к мусоропроводу. Задача в итоге будет решена, мусор исчезнет, но вы потратите в пять раз больше времени на хождение по коридору, чем на саму уборку.
В разработке происходит то же самое. Вместо того чтобы сразу забрать нужные зависимости, вы заставляете систему тратить ресурсы на лишние итерации.
Инженерный подход к оптимизации
Современные ORM позволяют использовать Eager Loading (жадную загрузку). Вы просто указываете в запросе, какие связи нужно подтянуть сразу, в первом же запросе.
Если же инструмент этого не поддерживает, есть рабочий лайфхак:
1. Делаете один запрос на все продукты.
2. Делаете второй запрос на все категории, относящиеся к этим продуктам (через
WHERE IN).3. Сопоставляете данные уже в памяти приложения.
Это в разы быстрее, чем долбить базу мелкими запросами.
N+1 часто всплывает при рефакторинге: кто-то убрал подгрузку релейшна, код не упал (магия ORM же), но производительность рухнула. Исправление одной такой точки может ускорить API буквально в сотни раз.
Запомните правило: любой запрос в базу данных внутри цикла — это в 90% случаев архитектурный провал. Всегда загружайте данные до того, как начнете их перебирать.
Ставь 🔥, если ловил N+1 в логах только после того, как база начинала «дымиться» на проде. А если остались вопросы или что-то звучит слишком абстрактно — пиши в комменты, обязательно разберем!
10МДК | ВЕБМастер
