Problemas que matan el rendimiento en Django ⚠️

Published: (February 28, 2026 at 03:11 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Índice 📑

Consultas N+1 🐌

Es un cuello de botella de rendimiento común en aplicaciones que interactúan con bases de datos. Ocurre cuando una aplicación ejecuta N consultas adicionales para recuperar datos que podrían haberse obtenido con una sola consulta. Esto da como resultado N + 1 consultas totales en lugar de solo 1, lo que genera una degradación significativa del rendimiento, especialmente a medida que crece el conjunto de datos.

Ejemplo

for post in Post.objects.all():
    print(post.author.name)

Esto genera 1 consulta para los posts + N consultas para los autores.

En Django se soluciona agregando el método select_related:

posts = Post.objects.select_related("author")

for post in posts:
    print(post.author.name)

Falta de índices en la base de datos 📚

Los índices optimizan la recuperación de información en una tabla, permitiendo consultas más rápidas sin necesidad de recorrer todos los registros. Es uno de los problemas más comunes en aplicaciones con grandes volúmenes de datos.

User.objects.filter(email="test@gmail.com")

Sin índices, ocurre un escaneo completo de la tabla. En producción, con millones de filas, provoca una degradación severa del rendimiento.

Para agregar un índice en el ORM de Django, debes usar db_index=True:

class User(models.Model):
    email = models.EmailField(db_index=True)

Evaluación prematura del QuerySet ⚡

Los QuerySet son lazy, pero muchos los evalúan de forma errónea:

qs = User.objects.all()
if len(qs) > 0:
    ...

Un uso correcto es verificar su existencia con exists():

if qs.exists():
    ...

El método exists() es útil para búsquedas relacionadas con la existencia de objetos; devuelve True si el QuerySet contiene resultados y False en caso contrario. Ejecuta la consulta de la forma más sencilla y rápida posible, pero no es adecuado cuando necesitas acceder a los objetos posteriormente.

Cargar datos innecesarios (overfetching) 📦

Al realizar una consulta donde necesitas solo algunos campos, es mejor limitar lo que se recupera.

Ejemplo que sobrecarga:

users = User.objects.all()

Obtener solo el nombre (además del id):

  • Con only() (retorna instancias del modelo):

    User.objects.only("name")
  • Con values() (retorna diccionarios):

    User.objects.values("name")
  • Con values_list() (retorna listas de tuplas o valores planos):

    User.objects.values_list("name", flat=True)

Acceder a objetos relacionados puede causar el problema de consulta N+1. Django proporciona select_related() y prefetch_related() para resolverlo y mejorar el rendimiento.

Recupera objetos relacionados en una única consulta SQL mediante JOIN. Es más adecuado para relaciones ForeignKey y OneToOneField.

Post.objects.select_related("author")

Ejecuta consultas separadas y combina los resultados en Python. Es más adecuado para relaciones ManyToMany, ForeignKey inversas y OneToOne inversas.

books = Book.objects.prefetch_related('authors')

for book in books:
    print(book.title)
    for author in book.authors.all():
        print(author.name)

Uso avanzado con Prefetch

Cuando necesitas filtrar, ordenar o asignar a un atributo personalizado, usa el objeto Prefetch.

from django.db.models import Prefetch

books = Book.objects.prefetch_related(
    Prefetch(
        "authors",
        queryset=Author.objects.filter(active=True)
    )
)

for book in books:
    for author in book.authors.all():
        print(author.name)

Uso incorrecto de count() 🔢

El método count() devuelve un número entero que representa la cantidad de objetos en la base de datos que coinciden con el QuerySet.

User.objects.count()

Nota: Si solo necesitas saber si existen registros, exists() es más eficiente que count() porque evita contar todos los resultados.

Un mal uso de devolver lo mismo que count()

len(User.objects.all())

count() ejecuta SELECT COUNT(*) directamente en la base de datos, mientras que len() carga todos los objetos en memoria.

No usar transacciones correctamente 🔒

Django utiliza autocommit por defecto, lo que significa que cada operación .save(), .create() o .update() se ejecuta como una transacción independiente.

Esto tiene dos consecuencias importantes:

  • Mayor overhead por múltiples commits.
  • Riesgo de inconsistencias si ocurre un error a mitad de una operación compleja.

Problema común

for item in items:
    item.processed = True
    item.save()

Django ejecuta 1 UPDATE = 1 COMMIT por cada iteración. Si tienes 10 000 objetos, tendrás 10 000 commits → overhead innecesario y mayor tiempo total de ejecución.

Solución con transaction.atomic()

from django.db import transaction

with transaction.atomic():
    for item in items:
        item.processed = True
        item.save()

Ahora Django ejecuta 10 000 UPDATE = 1 solo COMMIT (aunque no reduce el número de queries).

No usar bulk operations 🚀

bulk_create() inserta la lista de objetos proporcionada en la base de datos de manera eficiente y devuelve los objetos creados como una lista, en el mismo orden proporcionado:

users = [
    User(username="user1"),
    User(username="user2"),
]
User.objects.bulk_create(users)

Uso eficiente de annotate() 📊

annotate() permite agregar información calculada a cada objeto del QuerySet usando agregaciones SQL, evitando consultas adicionales innecesarias.

Problema común

posts = Post.objects.all()

for post in posts:
    print(post.title, post.comments.count())

Esto genera:

  • 1 consulta para los posts.
  • N consultas adicionales para contar los comentarios (problema N+1).

Solución con annotate()

from django.db.models import Count

posts = Post.objects.annotate(comment_count=Count("comments"))

for post in posts:
    print(post.title, post.comment_count)

Se realiza una sola consulta SQL, ya que el conteo se hace en la base de datos.

Ventajas

  • Reduce el número de consultas.
  • Aprovecha la optimización del motor de base de datos.
  • Mejora significativamente el rendimiento en grandes volúmenes de datos.

Otras funciones útiles

from django.db.models import Count, Sum, Avg, Max, Min

User.objects.annotate(
    post_count=Count("posts"),
    avg_score=Avg("posts__score")
)

No usar Django Debug Toolbar 🔎

django-debug-toolbar muestra información detallada sobre el rendimiento de tu aplicación en tiempo real, entre otras cosas:

  • Número de consultas SQL ejecutadas.
  • Tiempo de ejecución de cada consulta.
  • Queries duplicadas.
  • Tiempo total de respuesta.
  • Uso de caché.

Esto permite detectar problemas como:

  • Consultas N+1.
  • Consultas innecesarias.
  • Consultas lentas.
  • Falta de índices.

No usar Django Silk para profiling 🔬

Optimizar sin medir es uno de los errores más comunes. django-silk es una herramienta de profiling que permite analizar el rendimiento real de tu aplicación. A diferencia de Django Debug Toolbar, Django Silk:

  • Guarda historial de requests.
  • Permite analizar queries en detalle.
  • Muestra tiempo de ejecución exacto.
  • Permite profiling de funciones Python.

Linkografía 📖

0 views
Back to Blog

Related posts

Read more »