Проблема N+1 запросов

Проблема N+1 запросов

Проблема 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 запросов к базе данных.

Проблема N+1 запросов
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)

Андрей Писаревский

Автор: Андрей Писаревский 

PHP Team Lead.
Коммерческий опыт в программировании с 2010 года и экспертиза в полном цикле веб разработки: Frontend, Backend, QA, CI/CD, управление крупными командами и Enterprise проектами.

А так-же открыт к предложениям о работе со стеком PHP/Golang.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *