Skip to content
Moritz Klaßen

Moritz Klassen

Laravel 4 Min. Lesezeit

N+1-Queries in Laravel schnell finden und beheben

Eine Laravel-Seite wird zäh, obwohl der Code sauber aussieht? Meist stecken zu viele kleine Datenbankabfragen dahinter. Hier siehst du, wie du sie in wenigen Minuten aufspürst und mit einem Handgriff loswirst, ohne die halbe App umzubauen.

Eine N+1-Query entsteht, wenn du eine Liste aus der Datenbank holst und dann für jeden Eintrag nochmal eine eigene Abfrage abschickst. Du lädst also 30 Blogbeiträge mit einer Query, und danach fragt Laravel für jeden Beitrag einzeln den Autor nach. Aus einer Abfrage werden 31. Das ist die N+1: eine Abfrage für die Liste, plus N Abfragen für die Beziehungen. Wenn du das verstanden hast, behebst du damit die häufigste Ursache für langsame Seiten in Eloquent, und zwar ohne Caching-Zauber oder ein größeres Refactoring.

Die These für diesen Artikel ist einfach. Wenn deine Laravel-App zäh lädt und die Datenbank nicht offensichtlich überlastet ist, lohnt der erste Blick fast immer auf N+1-Queries. Meist findest du sie in wenigen Minuten und behebst sie mit einer Zeile.

Woran du das Problem erkennst

Bevor du etwas optimierst, willst du wissen, wie viele Abfragen eine Seite wirklich abschickt. Sonst reparierst du im Dunkeln. Das Bequemste im lokalen Umfeld ist Laravel Debugbar. Die zeigt dir unten im Browser eine Leiste mit der Anzahl der Queries pro Seitenaufruf. Wenn du auf einer Übersichtsseite plötzlich 150 Abfragen siehst, ist die Sache meist klar.

Kein Debugbar zur Hand? Dann reicht ein kurzer Listener, der jede Query mitschreibt. Den kannst du in der boot-Methode deines AppServiceProvider einbauen und danach wieder rausnehmen.

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

DB::listen(function ($query) {
    Log::info($query->sql, $query->bindings);
});

Danach lädst du die verdächtige Seite und schaust in storage/logs/laravel.log. Wenn dort dieselbe Abfrage mit nur wechselnder ID zwanzig Mal untereinander steht, hast du deine N+1 gefunden. Wer lieber mit einem richtigen Debugger arbeitet, findet in der Laravel-Welt inzwischen auch fertige Desktop-Tools fürs lokale Step-Debugging, die solche Abfragen sichtbar machen.

Der schnelle Fix: Eager Loading mit with()

Der Klassiker sieht so aus. Du holst Beiträge und greifst in der Schleife auf eine Beziehung zu:

$posts = Post::all();

foreach ($posts as $post) {
    echo $post->author->name;
}

Jeder Zugriff auf $post->author löst eine neue Abfrage aus. Der Fix ist ein einziges Wort: with(). Damit sagst du Eloquent, dass es die Autoren gleich mitladen soll, in einer zusätzlichen Abfrage statt in dreißig.

$posts = Post::with('author')->get();

foreach ($posts as $post) {
    echo $post->author->name;
}

Aus 31 Abfragen werden zwei. Mehrere Beziehungen gehen als Array, verschachtelte über die Punktnotation. Wenn ein Beitrag Autor und Kommentare braucht und die Kommentare wiederum ihren Verfasser, schreibst du das so:

$posts = Post::with(['author', 'comments.user'])->get();

Das ist der ganze Trick. Kein neues Package, keine Umstellung der Datenbank, und die Seite läuft danach wirklich deutlich flotter.

Der eigentliche Hack: N+1 gar nicht erst durchrutschen lassen

Einen Fix nachträglich zu suchen funktioniert. Noch praktischer ist es, wenn Laravel dich schon während der Entwicklung warnt, sobald du eine Beziehung ungeplant nachlädst. Genau das kann Eloquent seit einigen Versionen. Du aktivierst preventLazyLoading, und jede Lazy-Query wirft dann eine Exception, statt still im Hintergrund zu laufen.

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

Der Klammerausdruck sorgt dafür, dass die Prüfung nur außerhalb der Produktion greift. Lokal und im Testing fliegst du sofort auf die Nase, wenn du ein with() vergessen hast. In Produktion bleibt es ruhig, damit ein vergessener Fall dir nicht die Live-Seite zerlegt. So wird aus einem diffusen Performance-Problem ein konkreter Fehler, den du direkt beim Coden siehst.

Wenn with() nicht die ganze Miete ist

Zwei Fälle solltest du kennen, weil sie schnell wieder unnötige Last erzeugen. Der erste: Du willst nur die Anzahl verknüpfter Einträge, nicht die Einträge selbst. Wenn du pro Beitrag die Kommentare zählst, brauchst du sie nicht komplett zu laden. Dafür gibt es withCount.

$posts = Post::withCount('comments')->get();

// verfügbar als $post->comments_count

Der zweite Fall betrifft breite Tabellen. Mit with('author') holst du erstmal alle Spalten des Autors, auch die, die du gar nicht anzeigst. Bei großen Tabellen lohnt es sich, die Auswahl einzugrenzen. Wichtig dabei: Der Fremdschlüssel muss in der Auswahl bleiben, sonst kann Eloquent die Beziehung nicht mehr zuordnen.

$posts = Post::with('author:id,name')->get();

Das sind keine Pflichtübungen für jede Seite. Aber auf einer Übersicht mit vielen Zeilen und Beziehungen machen sie einen spürbaren Unterschied, ohne dass du an der Struktur der App etwas änderst.

Fazit

Langsame Laravel-Seiten haben selten eine exotische Ursache. Meistens schickt die App im Hintergrund viel zu viele kleine Abfragen los, weil eine Beziehung erst in der Schleife nachgeladen wird. Du findest das in Minuten über Debugbar oder einen kurzen Query-Log, behebst es mit with() und sorgst mit preventLazyLoading dafür, dass es gar nicht erst wieder passiert. Damit ist die häufigste Bremse raus, bevor du überhaupt über Caching oder besseres Hosting nachdenkst.

Häufige Fragen

Kurz beantwortet, damit du schneller einschätzen kannst, was für dein Projekt wichtig ist.

Was ist eine N+1-Query in Laravel?

Eine N+1-Query entsteht, wenn du eine Liste mit einer Abfrage lädst und danach für jeden einzelnen Eintrag eine weitere Abfrage abschickst, etwa für den Autor. Aus einer Abfrage werden so N zusätzliche. Das bremst vor allem Übersichtsseiten mit vielen Zeilen aus.

Wie behebe ich eine N+1-Query am schnellsten?

Mit Eager Loading über die Methode with(). Statt Post::all() schreibst du Post::with(‚author‘)->get(). Damit lädt Eloquent die Beziehung in einer einzigen zusätzlichen Abfrage mit, statt sie in der Schleife nachzuholen.

Wie verhindere ich N+1-Queries dauerhaft?

Aktiviere in der boot-Methode deines AppServiceProvider Model::preventLazyLoading(! app()->isProduction()). Dann wirft jede ungeplante Lazy-Query lokal und im Testing eine Exception, während die Produktion unberührt bleibt.

Kontakt

Lass uns über dein Projekt sprechen.

Ob neue Website, Laravel-Tool, Relaunch oder technischer Sparringstermin: Buch dir gern direkt einen Slot oder schreib mir eine Mail.

hello@moritzklassen.com