Проблема N+1 запросов — это распространенная проблема производительности, с которой сталкиваются разработчики при использовании объектно-реляционного отображения (ORM), включая Eloquent в Laravel. Эта проблема возникает, когда в процессе загрузки связанных данных для каждой записи из начального запроса выполняется дополнительный запрос к базе данных.
Рассмотрим пример на основе Laravel и Eloquent
Предположим, у нас есть модель Post и каждый Post может иметь множество Comment.
Если мы попытаемся загрузить все посты и их комментарии без оптимизации запросов, мы можем столкнуться с проблемой N+1 запросов следующим образом:
$posts = Post::all(); // Это "1" запрос для получения всех постов
foreach ($posts as $post) {
// Для каждого поста ("N" постов) выполняется еще один запрос для загрузки комментариев
$comments = $post->comments; // Это приводит к "N" дополнительным запросам к базе данных
}
В этом примере, если у нас есть 10 постов, мы получаем 1 запрос для получения всех постов, плюс 10 дополнительных запросов для загрузки комментариев для каждого поста, что в сумме дает 11 запросов к базе данных.

select count(*) as aggregate from `posts`
select * from `posts` limit 10 offset 0
select * from `categories` where `categories`.`id` = 2 limit 1
select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` = 8
select * from `categories` where `categories`.`id` = 2 limit 1
select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` = 9
select * from `categories` where `categories`.`id` = 6 limit 1
select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` = 10
Проблема в том, что это неэффективно и может значительно замедлить приложение, особенно если записей много.
Решение проблемы N+1 запросов в Laravel:
Laravel предлагает решение в виде «жадной загрузки» (eager loading) с помощью метода with(), который позволяет загрузить все необходимые связанные данные одним запросом:
$posts = Post::with('comments')->get(); // Загрузка всех постов и связанных с ними комментариев одним запросом
$posts = Post::with('category', 'tags')->paginate(10);
Используя with(), Laravel сформирует всего два запроса, независимо от количества постов:
- один для получения всех постов
- и один для получения всех комментариев к этим постам.
Затем Eloquent «разложит» комментарии по соответствующим постам в памяти, что значительно уменьшает нагрузку на базу данных и увеличивает производительность приложения.
SQL запросы полученные в итоге
Laravel сгенерирует два SQL запроса. Первый запрос извлекает все посты, а второй — все комментарии, связанные с этими постами. Запросы будут выглядеть примерно так:
SELECT * FROM `posts`;
SELECT * FROM `comments` WHERE `post_id` IN (1, 2, 3, ..., N);
В этом запросе IN-подзапрос будет содержать список идентификаторов всех постов, извлеченных первым запросом. Laravel автоматически обрабатывает результаты и «распределяет» комментарии по соответствующим постам на основе их post_id.
Таким образом, вместо того чтобы выполнять отдельный запрос на каждый пост для получения его комментариев (что приводило бы к проблеме N+1 запросов), Laravel извлекает все необходимые данные за меньшее количество запросов, что значительно улучшает производительность приложения при работе с большими наборами данных.

select count(*) as aggregate from `posts`
select * from `posts` limit 10 offset 0
select * from `categories` where `categories`.`id` in (2, 6)
select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` in (8, 9, 10)






