¿Por qué se menciona tanto el problema N+1 en Django ORM?

Al usar Django ORM, a menudo escuchamos el término "problema N+1". Sin embargo, es difícil sentir cuán grave es este problema a menos que lo experimentemos directamente.

En pocas palabras, se trata de un “problema en el que se ejecutan muchas más consultas de las que podrían ejecutarse con una sola”. Como resultado, la velocidad de carga de la página puede disminuir drásticamente, y a medida que aumentan los datos, la degradación del rendimiento se vuelve severa. Esto suele ocurrir especialmente al consultar datos que tienen relaciones entre modelos, por lo que es algo que debemos entender al utilizar ORM.

💡 Entendamos el problema N+1 fácilmente

Supongamos que estamos creando un sistema de blogs. Cada autor puede escribir varios artículos. Si lo expresamos en un modelo de Django, se vería así:

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)

Como cada autor puede escribir varios artículos, hay una relación 1:N.

Ahora supongamos que queremos implementar una función que imprime los nombres de todos los autores y los títulos de los artículos que han escrito. Generalmente, es probable que lo escribamos como sigue:

⚠️ Código donde ocurre el problema N+1

authors = Author.objects.all()  # (1) Primera consulta (Consulta tabla Author)

for author in authors:
    print(author.post_set.all())  # (N) Consulta individual de los Post de cada Author

Si lo ejecutamos una vez, puede que la carga tarde más de lo esperado. Si los datos no son muchos, podría no notarse, pero, ¿qué pasaría si hay 100 autores y más de 500 artículos? Ahora los problemas empiezan a hacerse evidentes.

🧐 Consultas SQL que se ejecutan realmente

SELECT * FROM author;  -- Ejecutado 1 vez
SELECT * FROM post WHERE author_id = 1;  -- Ejecutado N veces
SELECT * FROM post WHERE author_id = 2;
SELECT * FROM post WHERE author_id = 3;
...

A medida que aumentan los autores, la cantidad de consultas crece exponencialmente. Este es el problema N+1.

🚀 ¿En qué situaciones suele ocurrir el problema N+1?

En realidad, este problema es algo que debemos tener en cuenta casi siempre que consultamos datos relacionados entre dos o más modelos, ya sea que usemos bien o mal Django ORM. Suele ocurrir especialmente en los siguientes casos:

1️⃣ Cuando referenciamos datos dentro de un bucle en un template

{% for author in authors %}
    

{{ author.name }}

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

Cuando accedemos a datos en un bucle dentro de un template, se generan consultas adicionales por cada objeto, lo que degrada el rendimiento.

2️⃣ Cuando referenciamos modelos relacionados dentro de un bucle en una vista

def blog_view(request):
    authors = Author.objects.all()  # Primera consulta (Consulta Author)
    
    author_list = []
    for author in authors:
        author_list.append({
            'name': author.name,
            'posts': author.post_set.all()  # Se generan N consultas adicionales
        })
    
    return render(request, 'blog.html', {'authors': author_list})

El mismo problema puede surgir incluso cuando procesamos datos en una función de vista, no solo en templates.

💡 Mi experiencia personal con el problema N+1

Anteriormente, creé un método personalizado llamado get_absolute_url(). Era una función que generaba una URL combinando varios campos del modelo, y fue solo después de que experimenté una disminución del rendimiento que encontré la causa.

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}/"

Cada vez que llamaba a este método, se ejecutaba self.author.name, generando N consultas adicionales. Pasé un tiempo considerable solucionando este problema.

✅ Resumen: Razones por las cuales debemos entender el problema N+1

  • Mientras usemos Django ORM, este problema puede surgir en cualquier momento.
  • Si la velocidad de la página disminuye, es probable que el ORM esté ejecutando más consultas de las esperadas.
  • Puede ocurrir en templates, funciones de vista o métodos del modelo.
  • A medida que aumenta la cantidad de datos, la degradación del rendimiento puede volverse grave.
  • El ORM no optimiza automáticamente, por lo que debemos verificar y optimizar las consultas que se ejecutan.

En el siguiente artículo, abordaré cómo resolver el problema N+1 utilizando select_related y prefetch_related.

Problema de consulta N+1 en Django ORM