El hack del ORM que nadie te enseña: cómo detectar y eliminar las N+1 queries

Nivel: Intermedio · Tiempo: 15 minutos · Impacto: Inmediato en producción Prerequisito: Haber completado el Curso de Django en Platzi

🚨 El Problema: ¿Qué es una Query N+1?

Imagina que tienes el proyecto CoffeeShop del curso y quieres listar todas las órdenes con el nombre del cliente que las hizo.

Haces esto en tu vista y parece funcionar perfecto:

# views.py — Esto SE VE bien, pero ES un desastre silencioso
def lista_ordenes(request):
    ordenes = Order.objects.all()  # 1 query
    return render(request, 'ordenes.html', {'ordenes': ordenes})
<!-- ordenes.html -->
{% for orden in ordenes %}
    <p>{{ orden.user.username }} — {{ orden.fecha }}</p>  <!-- +1 query por cada orden 😱 -->
{% endfor %}

¿Qué pasa internamente?

AcciónQueries ejecutadas

Order.objects.all()

1 query

Acceder a orden.user en cada iteración

1 query × N órdenes

Total con 100 órdenes

101 queries 💀

Total con 1,000 órdenes

1,001 queries 💀💀💀

Tu app funciona en desarrollo con 5 registros. Llega a producción con miles y explota.

🛠️ Paso 1: Instala django-debug-toolbar para VER el problema

Primero necesitas ver el monstruo antes de matarlo.

pip install django-debug-toolbar

Agrega esto a tu settings.py:

# settings.py

INSTALLED_APPS = [
    # ... tus apps existentes ...
    'debug_toolbar',  # 👈 agrega esto
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',  # 👈 al inicio del middleware
    # ... resto de middlewares ...
]

# Permite que la toolbar se muestre en localhost
INTERNAL_IPS = [
    '127.0.0.1',
]

Agrega la URL en tu urls.py principal:

# urls.py
from django.conf import settings

urlpatterns = [
    # ... tus URLs existentes ...
]

# Solo activa la toolbar en desarrollo
if settings.DEBUG:
    import debug_toolbar
    urlpatterns = [
        path('__debug__/', include(debug_toolbar.urls)),
    ] + urlpatterns

Resultado: Ahora verás un panel lateral en tu app con el número exacto de queries por página.

💀 Paso 2: Reproduce el problema y ve el horror

Con la toolbar activa, entra a tu vista de órdenes.

En el panel verás algo como esto:

SQL Queries: 47 queries in 320ms

Haz clic y verás 47 queries casi idénticas como:

SELECT * FROM auth_user WHERE id = 1;
SELECT * FROM auth_user WHERE id = 2;
SELECT * FROM auth_user WHERE id = 3;
-- ... 44 veces más 😭

Esto es el problema N+1 en vivo. Ahora lo matamos.

⚡ Paso 3: El Fix — select_related y prefetch_related

Regla de oro:

Tipo de relaciónSolución

ForeignKey / OneToOneField

select_related()

ManyToManyField / reverse FK

prefetch_related()

Fix para ForeignKey (relación directa):

# views.py — ANTES 💀
def lista_ordenes(request):
    ordenes = Order.objects.all()
    return render(request, 'ordenes.html', {'ordenes': ordenes})

# views.py — DESPUÉS ⚡
def lista_ordenes(request):
    # select_related hace un SQL JOIN y trae todo en UNA sola query
    ordenes = Order.objects.select_related('user').all()
    return render(request, 'ordenes.html', {'ordenes': ordenes})

Resultado en la toolbar:

ANTES:  47 queries en 320ms  💀
DESPUÉS: 1 query  en  18ms   ⚡

Fix para ManyToMany (ej: productos de una orden):

# views.py — ANTES 💀
def detalle_orden(request, orden_id):
    orden = Order.objects.get(id=orden_id)
    # Acceder a orden.products.all() en el template genera N queries
    return render(request, 'detalle.html', {'orden': orden})

# views.py — DESPUÉS ⚡
def detalle_orden(request, orden_id):
    # prefetch_related hace una segunda query optimizada y cachea los resultados
    orden = Order.objects.prefetch_related('products').get(id=orden_id)
    return render(request, 'detalle.html', {'orden': orden})

El combo definitivo (relaciones encadenadas):

# Cuando necesitas acceder a user y a los productos al mismo tiempo
ordenes = Order.objects.select_related('user').prefetch_related('products').all()

Con esto le dices a Django: "Tráeme las órdenes, sus usuarios y sus productos, todo de una vez."

🎯 Paso 4: Aplícalo en el proyecto CoffeeShop del curso

En el proyecto del curso tienes el modelo Order con relación a User. Aquí está el before/after completo:

# orders/views.py — Versión optimizada lista para producción

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from .models import Order

class MyOrdersView(LoginRequiredMixin, ListView):
    model = Order
    template_name = 'orders/my_orders.html'
    context_object_name = 'orders'

    def get_queryset(self):
        return (
            Order.objects
            .select_related('user')           # 👈 trae el usuario en el JOIN
            .prefetch_related('products')      # 👈 trae los productos en batch
            .filter(user=self.request.user)
            .order_by('-created_at')
        )

🏆 Resultado Final

MétricaSin optimizarOptimizado

Queries en lista de órdenes

47

1

Tiempo de respuesta

~320ms

~18ms

Escalabilidad

Muere con 1k registros

Aguanta millones

💡 Tip Extra (el que pocos conocen)

Puedes usar only() y defer() para traer solo las columnas que necesitas y reducir aún más el peso de cada query:

# Solo trae id, fecha y el username del user — ignora todo lo demás
ordenes = (
    Order.objects
    .select_related('user')
    .only('id', 'created_at', 'user__username')  # 👈 columnas exactas
    .filter(user=request.user)
)

Ideal para vistas de listado donde no necesitas todos los campos del modelo.

📋 Resumen: Tu Checklist Anti N+1

  • [ ] Instala django-debug-toolbar en todos tus proyectos de desarrollo
  • [ ] Revisa que ninguna vista tenga más de 5-10 queries
  • [ ] Usa select_related() en cada ForeignKey que accedas en el template
  • [ ] Usa prefetch_related() en cada ManyToManyField o reverse FK
  • [ ] Combínalos cuando tengas relaciones múltiples
  • [ ] Usa only() en vistas de listado para máxima performance

🚀 ¿Quieres ir más lejos? Explora django-silk para profiling avanzado en producción y QuerySet.explain() para analizar el plan de ejecución de tus queries directamente desde Django.

Tutorial basado en el Curso de Django de Platzi — Nivel intermedio

0 Comentarios

para escribir tu comentario

Artículos relacionados