Eloquent N+1 systematisch finden und beheben
N+1 ist der häufigste Performance-Killer in Laravel-Apps. Mit preventLazyLoading, Debugbar und gezielten Query-Inspektionen finden Sie die Stellen, an denen es wehtut.
Warum dieser Leitfaden
N+1 ist selten ein Mysterium, sondern ein Reflex
In jeder Laravel-Anwendung, die wir in den letzten zwei Jahren refaktoriert haben, war N+1 unter den Top-3-Ursachen für langsame Listen-Views. Die gute Nachricht: Wenn Sie einmal die richtige Diagnose-Routine etablieren, taucht das Problem in Ihrem Team kaum noch in Production auf. Dieser Artikel zeigt den Workflow, den wir bei jeder neuen Codebase als Erstes einziehen.
Strict Mode: Lazy Loading komplett verbieten
Der schnellste Weg, jedes versteckte N+1 sichtbar zu machen, ist Model::preventLazyLoading(). Damit wirft Eloquent eine LazyLoadingViolationException, sobald irgendwo im Code eine Relation ohne with() angefragt wird. In lokaler Entwicklung und Staging einschalten, in Production ausschalten oder loggen statt werfen.
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::shouldBeStrict(! $this->app->isProduction());
// Alternativ feiner steuern:
Model::preventLazyLoading(! $this->app->isProduction());
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
Model::preventAccessingMissingAttributes(! $this->app->isProduction());
}
Debugbar als zweite Sicherheit
Die Laravel Debugbar zeigt Ihnen pro Request die Query-Liste mit Dauer und Stack-Trace. Achten Sie auf die „Queries“-Sektion: Taucht dort dieselbe SELECT-Anweisung mehrfach mit nur unterschiedlicher WHERE-Klausel auf, haben Sie ein klassisches N+1 gefunden. In CI ergänzen wir das durch Telescope-Snapshots auf Staging.
Code-Review benötigt?
Wir lesen mit, messen mit und liefern eine priorisierte Fix-Liste - inklusive Diff-Vorschlägen.
Der Fix
Vorher und nachher: ein konkretes Beispiel
Stellen Sie sich eine Blog-Index-Seite vor, die zu jedem Artikel den Autor und die ersten drei Kommentare anzeigt. Ohne Eager Loading produziert das pro Seite mit 20 Artikeln gerne 41 Queries. Mit korrektem with() landen wir bei 3. Das ist nicht akademisch - das ist der Unterschied zwischen 800 ms und 45 ms Time-to-First-Byte.
Vorher: jede Relation ein eigener Query
Klassisches Symptom: ein Controller, der nur ::all() oder ::paginate() macht, und ein Blade-Template, das munter $post->author->name und $post->comments aufruft. Funktional sauber, aber Eloquent feuert pro Iteration einen neuen Query - einmal für den Autor, einmal für die Kommentare.
public function index(): View
{
// Ein Query holt die Posts
$posts = Post::latest()->paginate(20);
// Im Template: $post->author->name → 1 Query je Post
// $post->comments → 1 Query je Post
// => 1 + 20 + 20 = 41 Queries
return view('posts.index', ['posts' => $posts]);
}
Nachher: with() lädt vor, withCount zählt vor
with() löst die Relationen in einen zweiten Query mit WHERE IN auf. withCount() ergänzt Aggregat-Spalten, ohne die Relation komplett zu laden. Für Listen-Views ist withCount oft der bessere Trade-off - Sie brauchen selten die vollständigen Kommentar-Objekte, nur die Anzahl.
public function index(): View
{
$posts = Post::query()
->with(['author:id,name,avatar_url'])
->withCount('comments')
->latest()
->paginate(20);
// 3 Queries gesamt: posts, authors (WHERE IN), comments-count
return view('posts.index', ['posts' => $posts]);
}
Regression verhindern
Einmal fixen, dauerhaft verhindern
Ein N+1 zu fixen ist die halbe Miete. Die andere Hälfte: dafür sorgen, dass derselbe Bug nicht in zwei Sprints wieder im Code landet. Zwei Mechanismen, die wir in jedem Projekt einbauen.
Query-Zähler in Feature-Tests
DB::enableQueryLog() in Tests aktivieren und die Anzahl der ausgeführten Queries gegen eine Obergrenze prüfen. Sobald Sie einen Eager-Load entfernen, schlägt der Test fehl - bevor die Änderung in Main landet.
use Illuminate\Support\Facades\DB;
public function test_index_view_keeps_query_count_under_budget(): void
{
Post::factory()->count(20)->create();
DB::enableQueryLog();
$this->get('/posts')->assertOk();
$queries = count(DB::getQueryLog());
// Hartes Limit. Steigt der Wert, fragen wir warum.
$this->assertLessThanOrEqual(5, $queries);
}
Artikel teilen
Hat das geholfen? Wir machen Audit, Workshop oder Coaching auch live
Pakete direkt im Shop. Unser Standard-Tagessatz liegt bei 1.500 € netto.