Django ORM: optimizar las consultas para un rendimiento máximo
Guía completa para optimizar las consultas del ORM de Django. select_related, prefetch_related, índices, análisis del problema N+1 y técnicas avanzadas para aplicaciones de alto rendimiento.

El ORM de Django ofrece una abstracción elegante para interactuar con la base de datos, pero esa simplicidad puede ocultar problemas de rendimiento críticos. Una aplicación Django mal optimizada puede generar cientos de consultas allí donde una sola sería suficiente. Esta guía explora las técnicas esenciales para identificar y resolver esos problemas.
Antes de optimizar, hay que medir. El uso de django-debug-toolbar en desarrollo permite visualizar cada consulta SQL generada e identificar rápidamente los cuellos de botella.
Comprender el problema N+1
El problema N+1 representa la trampa más habitual al trabajar con ORM. Aparece cuando una consulta inicial recupera N objetos y, a continuación, se ejecutan N consultas adicionales para acceder a las relaciones de cada objeto. Esta multiplicación de consultas degrada gravemente el rendimiento.
# models.py
from django.db import models
class Author(models.Model):
"""Modelo que representa al autor de un libro."""
name = models.CharField(max_length=200)
email = models.EmailField(unique=True)
bio = models.TextField(blank=True)
def __str__(self):
return self.name
class Book(models.Model):
"""Modelo que representa un libro con su autor."""
title = models.CharField(max_length=300)
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name='books'
)
published_date = models.DateField()
isbn = models.CharField(max_length=13, unique=True)
def __str__(self):
return self.titleEstos modelos sencillos permiten ilustrar el problema N+1 de forma concreta.
# views.py - Ejemplo problemático
def list_books_bad(request):
"""❌ Esta vista genera N+1 consultas."""
books = Book.objects.all() # 1 consulta para los libros
for book in books:
# Cada acceso a book.author dispara una consulta SQL
print(f"{book.title} por {book.author.name}")
# ¡Con 100 libros = 101 consultas SQL!
return render(request, 'books/list.html', {'books': books})Este código parece inocente pero genera una consulta por libro para recuperar al autor asociado.
Resolver el N+1 con select_related
El método select_related realiza un JOIN SQL y recupera los datos relacionados en una sola consulta. Funciona con relaciones ForeignKey y OneToOneField.
# views.py - Solución optimizada con select_related
def list_books_optimized(request):
"""✅ Esta vista genera una única consulta con JOIN."""
# select_related realiza un JOIN SQL
books = Book.objects.select_related('author').all()
for book in books:
# Sin consulta adicional: el autor ya está cargado
print(f"{book.title} por {book.author.name}")
# Total: 1 sola consulta SQL sin importar el número de libros
return render(request, 'books/list.html', {'books': books})La consulta SQL generada utiliza un LEFT OUTER JOIN para recuperar los autores junto con los libros.
# Encadenar select_related para relaciones anidadas
# models.py
class Publisher(models.Model):
name = models.CharField(max_length=200)
country = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
# views.py
def list_books_with_details(request):
"""Recupera libros, autores y editoriales en una sola consulta."""
books = Book.objects.select_related(
'author', # ForeignKey hacia Author
'publisher' # ForeignKey hacia Publisher
).all()
return render(request, 'books/list.html', {'books': books})Varias relaciones pueden optimizarse al mismo tiempo enumerándolas en select_related.
Optimizar las relaciones ManyToMany con prefetch_related
Para las relaciones ManyToMany o las relaciones inversas (ForeignKey desde el otro lado), prefetch_related ejecuta consultas separadas pero optimizadas, evitando JOINs masivos.
# models.py
class Tag(models.Model):
"""Etiquetas para categorizar los libros."""
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, related_name='books')La relación ManyToMany entre Book y Tag exige prefetch_related para una optimización eficaz.
# views.py - Optimización ManyToMany
def list_books_with_tags(request):
"""✅ Recupera los libros y sus etiquetas de manera eficiente."""
books = Book.objects.prefetch_related('tags').all()
for book in books:
# Las etiquetas están precargadas, sin consulta adicional
tag_names = [tag.name for tag in book.tags.all()]
print(f"{book.title}: {', '.join(tag_names)}")
# Total: 2 consultas (libros + etiquetas) sin importar la cantidad
return render(request, 'books/list.html', {'books': books})prefetch_related ejecuta una consulta separada para las etiquetas y luego realiza la unión en Python.
Utilizar select_related para ForeignKey y OneToOne (JOIN SQL). Utilizar prefetch_related para ManyToMany y relaciones inversas (consultas separadas). Ambos pueden combinarse en el mismo QuerySet.
Personalizar la precarga con objetos Prefetch
El objeto Prefetch permite un control fino sobre los datos precargados: filtrado, ordenación e incluso limitación del número de resultados.
# views.py
from django.db.models import Prefetch
def list_authors_with_recent_books(request):
"""Recupera los autores con solo sus libros recientes."""
# Prefetch personalizado: solo los libros desde 2025
recent_books_prefetch = Prefetch(
'books',
queryset=Book.objects.filter(
published_date__year__gte=2025
).order_by('-published_date'),
to_attr='recent_books' # Almacenado en un atributo personalizado
)
authors = Author.objects.prefetch_related(
recent_books_prefetch
).all()
for author in authors:
# Acceso a través del atributo personalizado
for book in author.recent_books:
print(f"{author.name}: {book.title}")
return render(request, 'authors/list.html', {'authors': authors})El atributo to_attr guarda los resultados en una lista de Python en lugar del manager habitual.
# Combinación avanzada: select_related + Prefetch
def list_authors_complete(request):
"""Ejemplo completo de optimización en varios niveles."""
authors = Author.objects.prefetch_related(
Prefetch(
'books',
queryset=Book.objects.select_related(
'publisher' # Optimiza también la editorial de cada libro
).prefetch_related(
'tags' # Y las etiquetas de cada libro
).filter(published_date__year=2026)
)
).all()
return render(request, 'authors/complete.html', {'authors': authors})Este enfoque reduce drásticamente el número de consultas para estructuras de datos complejas.
¿Listo para aprobar tus entrevistas de Django?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Usar only() y defer() para limitar las columnas
De forma predeterminada, Django recupera todas las columnas de la tabla. Para modelos con muchos campos o campos muy grandes, limitar las columnas mejora el rendimiento.
# views.py
def list_books_minimal(request):
"""Recupera solo las columnas necesarias."""
# only() especifica las columnas a incluir
books = Book.objects.only(
'id',
'title',
'published_date'
).select_related('author')
# Atención: acceder a un campo no incluido dispara una consulta
for book in books:
print(book.title) # OK, incluido
# print(book.isbn) # Generaría una consulta adicional
return render(request, 'books/list.html', {'books': books})El método only() crea un objeto «diferido» que carga únicamente las columnas indicadas.
# defer() para excluir columnas concretas
def list_authors_without_bio(request):
"""Excluye campos grandes que rara vez se utilizan."""
# defer() excluye las columnas indicadas
authors = Author.objects.defer(
'bio' # El TextField no se carga
).all()
for author in authors:
print(author.name) # OK
print(author.email) # OK
# author.bio cargaría el campo bajo demanda
return render(request, 'authors/list.html', {'authors': authors})defer() es el inverso de only(): las columnas listadas no se cargan inicialmente.
Optimizar con values() y values_list()
Cuando solo se necesitan ciertos valores y no objetos de modelo completos, values() y values_list() devuelven diccionarios o tuplas más ligeros.
# views.py
def get_book_titles(request):
"""Recupera solo los títulos como una lista."""
# values_list devuelve tuplas
titles = Book.objects.values_list('title', flat=True)
# Resultado: ['Libro 1', 'Libro 2', ...]
# values devuelve diccionarios
book_data = Book.objects.values('title', 'published_date')
# Resultado: [{'title': 'Libro 1', 'published_date': ...}, ...]
return render(request, 'books/titles.html', {'titles': titles})Estos métodos evitan la instanciación de objetos de modelo y reducen el consumo de memoria.
# Combinación con agregaciones
from django.db.models import Count, Avg
def get_author_statistics(request):
"""Estadísticas por autor sin cargar objetos completos."""
stats = Author.objects.values('name').annotate(
book_count=Count('books'),
avg_year=Avg('books__published_date__year')
).order_by('-book_count')
# Resultado: [{'name': 'Autor', 'book_count': 5, 'avg_year': 2024}, ...]
return render(request, 'authors/stats.html', {'stats': stats})La anotación permite realizar cálculos directamente en la base de datos.
Crear índices para acelerar las consultas
Los índices de la base de datos aceleran drásticamente las búsquedas. Django permite definirlos directamente en los modelos.
# models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=300, db_index=True)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
published_date = models.DateField()
isbn = models.CharField(max_length=13, unique=True)
status = models.CharField(max_length=20, default='available')
class Meta:
# Índices compuestos para consultas frecuentes
indexes = [
# Índice sobre la fecha de publicación (ordenación frecuente)
models.Index(
fields=['published_date'],
name='book_pub_date_idx'
),
# Índice compuesto para filtrar por autor + estado
models.Index(
fields=['author', 'status'],
name='book_author_status_idx'
),
# Índice parcial: solo libros disponibles
models.Index(
fields=['published_date'],
name='book_available_idx',
condition=models.Q(status='available')
),
]
# Ordenación por defecto que aprovecha el índice
ordering = ['-published_date']Estos índices mejoran el rendimiento de las consultas que filtran por esas columnas.
Los índices aceleran las lecturas pero ralentizan ligeramente las escrituras (INSERT, UPDATE). Conviene crearlos solo en columnas usadas con frecuencia en cláusulas WHERE, ORDER BY o JOIN.
Recurrir a consultas SQL crudas cuando sea necesario
Para consultas complejas u optimizaciones específicas del motor, las consultas SQL crudas ofrecen un control total.
# views.py
from django.db import connection
def get_books_with_raw_sql(request):
"""Consulta cruda para casos especiales."""
# Método 1: raw() para recuperar objetos del modelo
books = Book.objects.raw('''
SELECT b.*, a.name as author_name
FROM library_book b
INNER JOIN library_author a ON b.author_id = a.id
WHERE b.published_date > %s
ORDER BY b.published_date DESC
''', ['2025-01-01'])
return render(request, 'books/list.html', {'books': books})
def execute_custom_query(request):
"""Ejecución directa para consultas que no son SELECT."""
with connection.cursor() as cursor:
# Consulta con agregación compleja
cursor.execute('''
SELECT
a.name,
COUNT(b.id) as book_count,
AVG(EXTRACT(YEAR FROM b.published_date)) as avg_year
FROM library_author a
LEFT JOIN library_book b ON b.author_id = a.id
GROUP BY a.id, a.name
HAVING COUNT(b.id) > 2
ORDER BY book_count DESC
''')
results = cursor.fetchall()
return render(request, 'stats.html', {'results': results})Las consultas crudas evitan el ORM pero sacrifican la portabilidad entre bases de datos.
Analizar las consultas con django-debug-toolbar
La herramienta django-debug-toolbar permite visualizar todas las consultas SQL generadas por una vista de Django.
# settings.py - Configuración de la debug toolbar
INSTALLED_APPS = [
# ... otras aplicaciones
'debug_toolbar',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
# ... otros middlewares
]
# Mostrar la toolbar para peticiones locales
INTERNAL_IPS = ['127.0.0.1']
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.sql.SQLPanel', # Consultas SQL
'debug_toolbar.panels.timer.TimerPanel', # Tiempo de ejecución
'debug_toolbar.panels.cache.CachePanel', # Caché
]Esta configuración activa los paneles más útiles para optimizar.
# Registro de las consultas SQL en desarrollo
# settings.py
LOGGING = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}Este logging muestra cada consulta SQL en la consola, útil para detectar problemas N+1.
Cachear los QuerySets
Para los datos a los que se accede con frecuencia y se modifican poco, la caché evita consultas repetidas.
# views.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page
def list_featured_books(request):
"""Recupera los libros destacados con caché."""
cache_key = 'featured_books_list'
# Intento de recuperación desde la caché
books = cache.get(cache_key)
if books is None:
# Cache miss: ejecutar la consulta
books = list(
Book.objects.select_related('author')
.filter(featured=True)
.order_by('-published_date')[:10]
)
# Guardar en caché durante 5 minutos
cache.set(cache_key, books, timeout=300)
return render(request, 'books/featured.html', {'books': books})
# Decorador para cachear toda la vista
@cache_page(60 * 15) # Caché 15 minutos
def list_all_tags(request):
"""Lista todas las etiquetas (datos que se modifican poco)."""
tags = Tag.objects.annotate(
book_count=Count('books')
).order_by('-book_count')
return render(request, 'tags/list.html', {'tags': tags})La caché reduce la carga sobre la base de datos para datos estáticos.
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Conclusión
La optimización de consultas con el ORM de Django se apoya en unos cuantos principios fundamentales:
✅ Medir antes de optimizar: utilizar django-debug-toolbar para detectar problemas reales
✅ Eliminar el N+1: select_related para ForeignKey, prefetch_related para ManyToMany
✅ Limitar los datos: only(), defer(), values() para cargar solo lo necesario
✅ Indexar con criterio: crear índices en columnas filtradas u ordenadas con frecuencia
✅ Cachear de forma estratégica: los datos que cambian poco se benefician de la caché de Django
✅ Consultas crudas: como último recurso para optimizaciones específicas del motor
Aplicadas con método, estas técnicas transforman una aplicación Django lenta en un sistema eficiente capaz de gestionar grandes volúmenes de datos. El ORM de Django sigue siendo una herramienta potente cuando se dominan sus matices.
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

Django 5.2: Middleware Personalizado y Manejo de Señales para Entrevistas Técnicas
Guía completa sobre middleware personalizado y señales en Django 5.2. Implementación de middleware de logging, middleware asíncrono, señales pre_save/post_save y preguntas frecuentes de entrevista.

Preguntas de Entrevista Django y Python: Top 25 en 2026
Las 25 preguntas mas frecuentes en entrevistas de Django y Python. ORM, vistas, middlewares, DRF, signals y optimizacion con respuestas detalladas y ejemplos de codigo.

Django 5: Crear una API REST con Django REST Framework
Guia completa para construir una API REST profesional con Django 5 y DRF. Serializers, ViewSets, autenticacion JWT y buenas practicas explicadas paso a paso.