N+1 запросов в Laravel: как один foreach превращается в 101 SELECT и как это запретить

nikita-practice 1 час назад Краткое содержание статьи N+1 запросов в Laravel: как один foreach превращается в 101 SELECT и как это запретить 3 мин 1.1K PHP * Laravel * Классическая ситуация: страница со списком из 100...
Will Anthropic have the best AI model at the end of October 2026?
В сфере искусственного интеллекта произошло заметное событие. nikita-practice 1 час назад Краткое содержание статьи N+1 запросов в Laravel: как один foreach превращается в 101 SELECT и как это запретить 3 мин 1. 1K PHP * Laravel * Классическая ситуация: страница со списком из 100 постов локально открывается мгновенно, а на проде с реальными данными — заметно тормозит. Профайлер показывает не один тяжёлый запрос, а сотню лёгких одинаковых.
Это проблема N+1, и в Laravel её очень легко создать, потому что Eloquent по умолчанию загружает связи лениво. Откуда берётся N+1// app/Models/Post. php class Post extends $posts = Post::latest()->take(100)->get(); // 1 запрос foreach ($posts as $post) { echo $post->author->name; // +1 запрос на КАЖДЫЙ пост } Обращение к $post->author — это не поле, а магическое свойство, которое при первом доступе выполняет SELECT * FROM users WHERE id = ?.
Технические детали
Итого 1 запрос за список + N запросов за авторов. img1-diagramВ Blade это выглядит ещё невиннее — {{ $post->author->name }} внутри @foreach, и никакого «запроса» глазами не видно. Как увидеть проблемуСамый простой способ без пакетов — посчитать запросы:use Illuminate\Support\Facades\DB; DB::enableQueryLog(); $posts = Post::latest()->take(100)->get(); foreach ($posts as $post) { $post->author->name; } dump(count(DB::getQueryLog())); // 101 Или логировать каждый запрос в dev-окружении:// AppServiceProvider::boot() DB::listen(function ($query) { logger()->debug($query->sql, $query->bindings); }); Удобнее всего — Laravel Debugbar или Telescope: там сразу видно количество запросов и одинаковые повторяющиеся SQL.
Решение: eager loading$posts = Post::with('author ->latest()->take(100)->get(); foreach ($posts as $post) { echo $post->author->name; // запросов больше нет } Теперь Eloquent делает ровно два запроса: посты и SELECT * FROM users WHERE id IN (... ), после чего сам раскладывает авторов по моделям. img2-before-afterПолезные варианты:// Несколько связей и вложенные связи Post::with()->get(); // Только нужные колонки (внешний ключ id обязателен!
) Post::with('author:id,name ->get(); // Нужно только количество — не грузим сами комментарии Post::withCount('comments ->get(); // $post->comments_count // Коллекция уже загружена — догружаем связь одним запросом $posts->load('author $posts->loadMissing('author // только если ещё не загружена Почему withCount лучше, чем $post->comments->count()$post->comments->count() сначала загружает все комментарии каждого поста в память (и без with — ещё и N запросов), а потом считает элементы коллекции. $post->comments()->count() со скобками делает SELECT COUNT(*) — но тоже на каждой итерации, то есть снова N+1. withCount('comments добавляет подзапрос в основной SELECT, и вы получаете готовое поле comments_count за один запрос.
Событие, по словам экспертов, усилит конкуренцию в сфере ИИ.



