Aller au contenu principal
{}
Retour à tous les articles

Article technique

Reconnaître et éviter les requêtes N+1 avec Eloquent

Analysez les requêtes de relations dans une liste Laravel. Contrôlez le préchargement, les relations absentes et la pagination.

Par Jordy L’Oeuille4 minutes de lecture
  • SQL
  • MySQL
  • Laravel
Nœud API central relié à d’autres services et tableaux de bord.
Dans cet article

Le N+1 apparaît lorsqu’une requête de liste est suivie d’une requête de relation par élément. Avec dix livres et un auteur non préchargé, un exemple simple peut exécuter une requête de liste puis dix requêtes. Le chargement anticipé peut ramener cette relation simple à deux requêtes. Ce n’est pas un benchmark client.

Utilisez les mêmes dix éléments avant et après. Comptez uniquement ce parcours et testez auteur absent et pagination. Relations polymorphes, scopes et autres relations peuvent produire des nombres différents.

  • Identifier boucle et relation puis compter les requêtes du vrai parcours.
  • Utiliser with pour les relations utiles et withCount pour un simple nombre.
  • Utiliser preventLazyLoading en développement lorsque pertinent et ajouter un test de régression.

Rendre visibles les requêtes de relations

Une liste peut être lente alors que chaque requête SQL paraît assez rapide. Dans cet exemple, Book possède une relation belongsTo author via author_id. Lire un auteur non chargé pour chaque livre peut interroger plusieurs fois la base après la requête initiale. Les modèles et champs sont des hypothèses pédagogiques, pas des modèles présents sur ce site. Mesurez le parcours complet avec Blade, les ressources API et la sérialisation : un simple accès à une propriété peut y déclencher une requête. Un index ne supprime pas ces appels répétés par l’application.

Comparer la même liste sans et avec préchargementexamples/book-authors.phpphp
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.

Compter uniquement le travail étudié

Avec dix livres ayant des auteurs existants, sans préchargement automatique ni scopes supplémentaires, le premier parcours effectue généralement une requête de liste puis dix requêtes de relation. Le second charge normalement cette relation simple en une requête supplémentaire. Cela explique un mécanisme, pas un résultat mesuré chez un client. Les nombres changent avec les clés absentes, relations polymorphes, scopes globaux, préchargement automatique ou pagination. Documentez ces conditions avant de fixer une limite dans un test. Distinguez aussi les allers-retours SQL du temps total, du réseau et du traitement PHP.

Ajouter un contrôle de régression ciblé

Exemple de test pour une application Book/Author existantetests/Feature/BookAuthorQueriesTest.phpphp
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();
    }
});

Les factories s’exécutent avant le début du comptage : on mesure la liste et ses relations, pas la préparation. Ce test suppose une base de test vide, la même connexion par défaut pour les deux modèles et aucune requête additionnelle liée aux événements ou scopes. Adaptez-le à vos modèles et à leur isolation. Il n’a pas été exécuté sur un modèle Book dans ce dépôt. Contrôlez aussi les valeurs retournées : réduire le nombre de requêtes ne suffit pas si des auteurs disparaissent, si le tri change ou si un autre compte devient visible.

Traiter les relations absentes et grandes listes

Conservez author_id dans la sélection des livres et id dans celle des auteurs. Testez un livre sans auteur et un auteur exclu par un scope de visibilité. Utilisez withCount lorsque la page demande seulement des nombres plutôt que les modèles complets. Charger toutes les relations gaspillerait alors de la mémoire. Le préchargement ne justifie pas non plus de récupérer des milliers de livres en une requête HTTP. Examinez la pagination et son comptage, puis mesurez mémoire et temps de réponse. Préservez les contraintes d’autorisation : optimiser ne doit jamais élargir l’accès aux données.

Éviter la réapparition de la boucle

Lisez la relation chargée comme une propriété. Un nouvel appel author()->first() peut encore lancer une requête au lieu de réutiliser l’objet disponible. Model::preventLazyLoading aide à détecter les chargements imprévus en développement lorsque l’application définit le traitement des violations. Contrôlez également les nouveaux fragments Blade et les ressources API. Si une requête lourde subsiste après la suppression du N+1, étudiez ensuite son plan et ses index. Il s’agit de causes différentes avec des corrections différentes. Gardez des critères de validation distincts et ne présentez pas le préchargement comme une solution universelle.

Poursuivre l’analyse

Un plan de requête ne représente qu’une partie du diagnostic. Les références ci-dessous relient l’analyse technique à un service SQL délimité. Les démonstrations utilisent des données fictives et non les résultats d’une mission client. Pour une demande, commencez par décrire le problème avec un contexte anonymisé, sans mot de passe, dump complet ni données personnelles.

Dernière mise à jour :

Besoin d’un contexte technique pour votre projet ?

Codegenie accompagne les applications Laravel, les codebases existantes, les déploiements mutualisés et les améliorations techniques.