Technisch artikel
N+1-query’s in Eloquent herkennen en voorkomen
Onderzoek relationele queries in een Laravel-lijst. Gebruik gerichte eager loading en controleer queryaantallen, ontbrekende relaties en paginering.
- SQL
- MySQL
- Laravel

In dit artikel
N+1 ontstaat wanneer een lijstquery gevolgd wordt door een relatiequery per record. Bij tien boeken en een niet-geladen auteurrelatie kan het eenvoudige patroon één lijstquery plus tien relatiequeries uitvoeren. Met bewuste eager loading kan dezelfde eenvoudige relatie in twee queries geladen worden. Dit is een verklaard voorbeeld, geen klantbenchmark.
Gebruik dezelfde tien records vóór en na de wijziging en tel alleen de queries van deze flow. Controleer ook een ontbrekende auteur en paginering. Polymorfe relaties, extra scopes en andere relaties kunnen andere aantallen opleveren.
- Leg de lus en gebruikte relatie vast; tel queries tijdens de echte route.
- Gebruik with voor benodigde relaties en withCount wanneer alleen een aantal nodig is.
- Activeer preventLazyLoading waar passend in ontwikkeling en voeg een regressietest op de flow toe.
Maak de verborgen relatiequeries zichtbaar
Een lijst kan traag zijn terwijl iedere afzonderlijke SQL-query redelijk snel is. In dit voorbeeld heeft Book een belongsTo-relatie author via author_id. Een lus die voor ieder boek de auteur opvraagt kan na de lijstquery opnieuw de database benaderen. De modelnamen en velden zijn aannames voor dit uitlegvoorbeeld, geen aanwezige modellen in deze website. Meet de echte route inclusief Blade, API-resources en serialisatie: juist daar kan een ogenschijnlijk onschuldige eigenschap een extra query veroorzaken.
use App\Models\Book;
// Assumptions: Book belongsTo Author through author_id; Author has id and name.
// Use a dedicated test dataset with ten books and existing non-null authors.
// The application has no automatic eager loading, default $with or extra scopes.
$before = [];
foreach (Book::query()->orderBy('id')->limit(10)->get() as $book) {
$before[] = ['id' => $book->id, 'author' => $book->author?->name];
}
$after = [];
$books = Book::query()
->select(['id', 'author_id']) // Keep the relationship foreign key.
->with('author:id,name') // Keep the related primary key as well.
->orderBy('id')
->limit(10)
->get();
foreach ($books as $book) {
$after[] = ['id' => $book->id, 'author' => $book->author?->name];
}
// In a test, compare these arrays for identical records, names and ordering.
// The expected query count depends on the documented relationship assumptions.Tel alleen wat je werkelijk onderzoekt
Voor tien boeken met bestaande auteurs geeft het eenvoudige patroon zonder automatische eager loading doorgaans één lijstquery plus tien relatiequeries. De tweede variant haalt de eenvoudige relatie doorgaans in één aanvullende query op. Dit is een verklaring van het patroon, geen gemeten klantresultaat. Queryaantallen veranderen bij ontbrekende foreign keys, polymorfe relaties, globale scopes, automatische eager loading of paginering. Leg die omstandigheden vast voordat je een harde grens in een test opneemt.
Voeg een afgebakende regressiecontrole toe
use App\Models\Author;
use App\Models\Book;
use Illuminate\Support\Facades\DB;
// Requires your application's factories, relationship and isolated test database.
// Never enable an unbounded query log during production traffic.
it('loads a simple author relation with a bounded query count', function (): void {
Book::factory()->count(10)->for(Author::factory(), 'author')->create();
DB::flushQueryLog();
DB::enableQueryLog();
try {
$books = Book::query()
->with('author:id,name')
->orderBy('id')
->limit(10)
->get(['id', 'author_id']);
$names = $books->map(fn (Book $book) => $book->author?->name)->all();
$queries = DB::getQueryLog();
} finally {
DB::disableQueryLog();
DB::flushQueryLog();
}
expect($books)->toHaveCount(10)
->and($queries)->toHaveCount(2);
foreach ($names as $name) {
expect($name)->toBeString()->not->toBeEmpty();
}
});De factory-opbouw gebeurt vóór het tellen. Zo meet je de lijst en het benaderen van de relatie, niet het maken van fixtures. De test veronderstelt een lege testdatabase, dezelfde standaardverbinding voor beide modellen en geen extra query’s van model-events of scopes. Pas hem aan je werkelijke model aan. Hij is niet uitgevoerd tegen een Book-model in deze repository. Controleer daarnaast de inhoud: een lager queryaantal is niet voldoende wanneer auteurs verdwijnen of records van een ander account zichtbaar worden.
Behandel ontbrekende relaties en grote lijsten
Bewaar author_id wanneer je kolommen beperkt en bewaar id in de auteurselectie. Test een boek zonder auteur en een auteur die door een scope niet zichtbaar is. Gebruik withCount wanneer de pagina alleen aantallen nodig heeft; alle gerelateerde records laden kost dan onnodig geheugen. Eager loading is evenmin een reden om duizenden boeken tegelijk te laden. Onderzoek paginering en de aparte telling, en meet naast queryaantallen ook geheugen en responstijd.
Voorkom dat de lus later terugkomt
Benader de geladen relatie als eigenschap. Een relatie opnieuw opvragen via author()->first() kan alsnog een query uitvoeren. Model::preventLazyLoading kan in ontwikkeling ongeplande relatiequeries helpen vinden, mits de toepassing expliciet bepaalt hoe overtredingen worden behandeld. Controleer ook nieuwe Blade-partials en API-resources. Als er na het verwijderen van N+1 nog één zware query overblijft, onderzoek dan pas dat uitvoeringsplan en de indexen. Dat zijn twee verschillende oorzaken met een andere oplossing.
Verder onderzoeken
Een queryplan is één deel van de diagnose. Hieronder vind je aanvullende uitleg en een afgebakende SQL-dienst. De demonstraties gebruiken fictieve gegevens, niet de resultaten van een klantproject. Deel bij een aanvraag alleen een probleembeschrijving en geanonimiseerde context; geen wachtwoorden, volledige dumps of persoonsgegevens.
Laatst bijgewerkt:


