La optimización de consultas es crucial en Django para evitar el problema N+1 y mejorar el rendimiento.
Django proporciona dos métodos principales: select_related y prefetch_related.
Ambos reducen el número de consultas a la base de datos, pero funcionan de manera diferente.
Antes de entender las soluciones, veamos el problema:
# Modelos de ejemplo
from django.db import models
class Autor(models.Model):
nombre = models.CharField(max_length=200)
nacionalidad = models.CharField(max_length=100)
def __str__(self):
return self.nombre
class Libro(models.Model):
titulo = models.CharField(max_length=300)
autor = models.ForeignKey(Autor, on_delete=models.CASCADE)
precio = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return self.titulo
class Categoria(models.Model):
nombre = models.CharField(max_length=100)
libros = models.ManyToManyField(Libro, related_name='categorias')
def __str__(self):
return self.nombre # PROBLEMA N+1: Una consulta por cada libro + una inicial
libros = Libro.objects.all() # 1 consulta
for libro in libros:
print(f"{libro.titulo} - {libro.autor.nombre}") # N consultas adicionales
# Si hay 100 libros = 101 consultas totales! select_related usa JOIN en SQL para traer datos relacionados en una sola consulta.
# SOLUCIÓN con select_related
libros = Libro.objects.select_related('autor').all()
for libro in libros:
print(f"{libro.titulo} - {libro.autor.nombre}") # Solo 1 consulta total!
# SQL generado (aproximado):
# SELECT libro.*, autor.* FROM libro
# INNER JOIN autor ON libro.autor_id = autor.id # Ejemplo 1: Acceso básico
libros_con_autor = Libro.objects.select_related('autor')
# Ejemplo 2: Múltiples relaciones
# Si Autor tuviera una relación con País
libros_completos = Libro.objects.select_related('autor', 'autor__pais')
# Ejemplo 3: En vistas de Django
def lista_libros(request):
libros = Libro.objects.select_related('autor').all()
return render(request, 'libros.html', {'libros': libros}) prefetch_related hace consultas separadas y las une en Python.
select_related no es aplicable# SOLUCIÓN con prefetch_related
categorias = Categoria.objects.prefetch_related('libros').all()
for categoria in categorias:
print(f"Categoría: {categoria.nombre}")
for libro in categoria.libros.all(): # Sin consultas adicionales
print(f" - {libro.titulo}")
# SQL generado (2 consultas separadas):
# 1. SELECT * FROM categoria
# 2. SELECT * FROM libro WHERE id IN (lista_de_ids) # Ejemplo 1: ManyToMany básico
categorias_con_libros = Categoria.objects.prefetch_related('libros')
# Ejemplo 2: Reverse ForeignKey
autores_con_libros = Autor.objects.prefetch_related('libro_set')
# O si usas related_name:
autores_con_libros = Autor.objects.prefetch_related('libros')
# Ejemplo 3: Prefetch anidado
categorias_completas = Categoria.objects.prefetch_related(
'libros__autor' # Libros y sus autores
) Puedes usar ambos métodos juntos para optimizaciones complejas:
# Combinación poderosa
libros_optimizados = Libro.objects.select_related(
'autor' # JOIN para autor
).prefetch_related(
'categorias' # Consulta separada para categorías
)
for libro in libros_optimizados:
print(f"Libro: {libro.titulo}")
print(f"Autor: {libro.autor.nombre}") # Sin consulta extra
print("Categorías:")
for categoria in libro.categorias.all(): # Sin consulta extra
print(f" - {categoria.nombre}") | Aspecto | select_related | prefetch_related |
|---|---|---|
| Tipo de relación | ForeignKey, OneToOne | ManyToMany, Reverse FK |
| Método SQL | JOIN | Consultas separadas |
| Número de consultas | 1 consulta | 2+ consultas |
| Memoria | Menos uso | Más uso (datos en Python) |
| Flexibilidad | Limitado | Más flexible |
# ERROR COMÚN: Usar select_related con ManyToMany
# Esto NO funcionará
libros = Libro.objects.select_related('categorias') # Error!
# CORRECTO: Usar prefetch_related
libros = Libro.objects.prefetch_related('categorias')
# ERROR: Usar prefetch_related innecesariamente
libros = Libro.objects.prefetch_related('autor') # Ineficiente
# CORRECTO: Usar select_related
libros = Libro.objects.select_related('autor') from django.db.models import Prefetch
# Prefetch personalizado con filtros
autores_con_libros_caros = Autor.objects.prefetch_related(
Prefetch(
'libros',
queryset=Libro.objects.filter(precio__gt=50),
to_attr='libros_caros'
)
)
for autor in autores_con_libros_caros:
print(f"Autor: {autor.nombre}")
for libro in autor.libros_caros: # Solo libros > $50
print(f" - {libro.titulo}: ${libro.precio}") Usa django-debug-toolbar o connection.queries para monitorear consultas:
from django.db import connection
print(len(connection.queries)) # Número de consultas select_related y prefetch_related son herramientas esenciales para optimizar Django.
La clave está en entender cuándo usar cada uno según el tipo de relación.
Una aplicación bien optimizada puede reducir las consultas de cientos a solo unas pocas.