Pourquoi le problème N+1 est-il si souvent mentionné dans Django ORM ?

Lorsque vous utilisez Django ORM, vous entendez souvent parler du "problème N+1". Cependant, il peut être difficile de comprendre la gravité de ce problème sans l'avoir vécu directement.

En termes simples, il s'agit d'un "problème où une seule requête censée être exécutée est exécutée beaucoup plus souvent que prévu". Cela peut entraîner un ralentissement significatif de la vitesse de chargement des pages, et plus il y a de données, plus la dégradation des performances est sévère. En particulier, cela se produit souvent lors de la consultation de données ayant des relations entre modèles, c'est pourquoi il est essentiel de bien comprendre ce concept lors de l'utilisation d'ORM.

💡 Comprenons facilement le problème N+1

Supposons que nous créions un système de blog. Chaque Auteur peut écrire plusieurs Articles. Cela peut être représenté par les modèles Django suivants.

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

Chaque Auteur peut écrire plusieurs Articles, établissant ainsi une relation 1:N.

Maintenant, supposons que nous souhaitons implémenter une fonctionnalité qui affiche le nom de chaque Auteur et les titres des Articles qu'il a écrits. Cela pourrait typiquement être rédigé comme ceci.

⚠️ Le code où le problème N+1 se produit

authors = Author.objects.all()  # (1) Première requête (consultation de la table Author)

for author in authors:
    print(author.post_set.all())  # (N) Consultation individuelle des Articles de chaque Auteur

En l'exécutant une fois, vous constaterez que le chargement prend plus de temps que prévu. Cela peut ne pas être perceptible si les données ne sont pas volumineuses, mais si vous avez 100 Auteurs et plus de 500 Articles ? Le problème devient alors visible.

🧐 Requêtes SQL réellement exécutées

SELECT * FROM author;  -- Exécution une fois
SELECT * FROM post WHERE author_id = 1;  -- Exécution N fois
SELECT * FROM post WHERE author_id = 2;
SELECT * FROM post WHERE author_id = 3;
...

Au fur et à mesure que le nombre d'Auteurs augmente, le nombre de requêtes augmente de manière exponentielle. C'est cela, le problème N+1.

🚀 Dans quelles situations le problème N+1 se produit-il fréquemment ?

En réalité, ce problème doit être pris en compte presque inévitablement lorsque deux ou plusieurs modèles sont liés dans une requête de données, que l'on utilise Django ORM de manière optimale ou non. Il se produit souvent dans les cas suivants.

1️⃣ Accès aux données dans une boucle dans un modèle de template

{% for author in authors %}
    

{{ author.name }}

    {% for post in author.post_set.all %}
  • {{ post.title }}
  • {% endfor %}
{% endfor %}

Lorsque vous accédez aux données dans une boucle du template, des requêtes supplémentaires sont générées pour chaque objet, ce qui affecte les performances.

2️⃣ Référence à des modèles associés dans une boucle dans la fonction de vue

def blog_view(request):
    authors = Author.objects.all()  # Première requête (consultation des Auteurs)
    
    author_list = []
    for author in authors:
        author_list.append({
            'name': author.name,
            'posts': author.post_set.all()  # Génère N requêtes supplémentaires
        })
    
    return render(request, 'blog.html', {'authors': author_list})

Un problème similaire peut survenir lorsque les données sont traitées dans une fonction de vue plutôt que dans un template.

💡 Mon expérience avec le problème N+1

Autrefois, j'ai créé une méthode personnalisée appelée get_absolute_url(). Cette fonction générait une URL en combinant plusieurs champs du modèle, mais ce n'est qu'après avoir rencontré des problèmes de performances que j'ai pu identifier la cause.

class Post(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

    def get_absolute_url(self):
        return f"/author/{self.author.name}/{self.slug}/"

Chaque fois que cette méthode est appelée, self.author.name est exécuté, générant ainsi N requêtes supplémentaires. Il m'a fallu pas mal de temps pour résoudre ce problème.

✅ Résumé : Pourquoi vous devez comprendre le problème N+1

  • Si vous utilisez Django ORM, ce problème peut se produire à tout moment.
  • Si la vitesse de la page ralentit, il est probable que l'ORM exécute plus de requêtes que prévu.
  • Ce problème peut survenir dans les templates, les fonctions de vue et les méthodes de modèle.
  • Plus vous avez de données, plus la dégradation des performances peut être grave.
  • L'ORM ne fera pas automatiquement les optimisations, donc vous devez vérifier et optimiser les requêtes exécutées.

Dans le prochain article, nous aborderons comment résoudre le problème N+1 en utilisant select_related et prefetch_related.

Problème de requête N+1 dans Django ORM