Eviter les requêtes N+1
Introduction
Lorsqu’une application affiche une liste d’entités et des informations associées, il est fréquent de parcourir cette liste pour accéder à une propriété de navigation. Cette approche paraît naturelle, mais elle peut déclencher un nombre important de requêtes SQL : c’est le problème des requêtes N+1.
Cela se produit lorsqu’une première requête récupère une collection de données, puis qu’une requête supplémentaire est exécutée pour chacune des entités afin de récupérer les données d'une relation associée. Au final, les données affichées sont correctes, mais le nombre d’allers-retours entre l’application et la base de données sont nombreux, ce qui peut dégrader les performances, notamment lorsque la collection contient de nombreux objets.
L’objectif consiste donc à identifier les données réellement nécessaires et à les récupérer efficacement en une seule requête.
Le problème des requêtes N+1
Prenons l’exemple d’une application qui affiche une liste de clients ainsi que le nombre de leurs commandes.
Avec le chargement différé (lazy loading) activé, le code suivant charge d’abord les clients :
Puis, l’accès à la propriété de navigation Orders dans le traitement itératif peut déclencher une requête SQL distincte pour chaque client, afin d'obtenir le nombre de commandes :
Si la requête initiale retourne 100 clients, EF Core peut exécuter :
-
Une requête pour récupérer les clients.
-
Puis une requête pour récupérer les commandes de chacun des 100 clients.
L’application exécute alors jusqu’à 101 requêtes SQL. C’est ce qu'on appelle des requêtes N+1.
Le problème est parfois difficile à identifier, car le code fonctionne correctement et les temps d’exécution restent acceptables avec peu de données. Il devient en revanche beaucoup plus visible lorsque le nombre de clients augmente car le temps d'affichage des données augmente aussi.
Charger les données associées avec Include
Lorsque l’application a besoin d'informations sur les clients et leurs commandes, il est possible de les charger explicitement avec la méthode Include :
var customers = await context.Customer
.Include(c => c.Orders)
.ToListAsync();
foreach (Customer customer in customers)
Console.WriteLine(customer.Orders.Count);
EF Core récupère alors les clients et leurs commandes dans le cadre du chargement initial. L’accès à propriété de navigation Orders dans le traitement itératif n'engendre plus l'exécution de requêtes SQL supplémentaires. Toutefois, il n’est pas toujours utile de charger toutes les colonnes de toutes les commandes pour en obtenir le nombre.
Privilégier la projection lorsque seules certaines données sont nécessaires
Dans le cas d’une liste affichant uniquement le nom du client et son nombre de commandes, une projection est généralement plus efficace :
var customers = await context.Customers
.Select(c => new
{
c.FirstName,
c.LastName,
OrderCount = c.Orders.Count
})
.ToListAsync();
EF Core traduit cette expression LINQ en une requête SQL adaptée. Seules les données nécessaires sont récupérées : le prénom, le nom et le nombre de commandes. Cette approche présente plusieurs avantages : elle évite les requêtes N+1 et limite les données transférées depuis la base de données.