Por defecto, Django carga todos los campos de un modelo en cada consulta. En una vista de lista con 50 artículos, eso significa obtener el contenido completo, el resumen, los metadatos y los campos de traducción 50 veces, aunque solo se muestre el título y la fecha. Cuatro métodos QuerySet de Django permiten controlar exactamente qué se carga: defer(), only(), values_list() y Prefetch(). El resultado: 2 consultas SQL en lugar de N+2, con únicamente las columnas necesarias.
Django defer(): excluir los campos pesados del QuerySet
Django defer() indica al ORM que excluya ciertos campos de la consulta inicial. Los campos excluidos siguen siendo accesibles en la instancia, pero cada acceso lanza una consulta adicional.
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField()
content = models.TextField() # campo pesado
excerpt = models.TextField() # campo pesado
is_published = models.BooleanField(default=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
published_at = models.DateTimeField()
# Vista de lista: el contenido no se muestra
posts = Post.objects.filter(is_published=True).defer("content", "excerpt")
SQL generado:
SELECT id, title, slug, is_published, author_id, published_at
FROM blog_post
WHERE is_published = true;
La trampa clásica: acceder a un campo diferido dentro de un bucle.
# ❌ N+1 silencioso: 1 consulta extra por post
for post in Post.objects.defer("content"):
print(post.content) # lanza SELECT content FROM blog_post WHERE id = ?
# ✅ Si el contenido es necesario, no usar defer()
for post in Post.objects.all():
print(post.content)
defer() es útil cuando se controlan estrictamente los campos renderizados. En un serializador que accede a todo, resulta contraproducente. Ver también in_bulk() para limitar las consultas N+1 en operaciones masivas.
Las llamadas a defer() se acumulan:
Post.objects.defer("content").defer("excerpt")
# → excluye content Y excerpt (equivalente a defer("content", "excerpt"))
Django only(): cargar únicamente los campos necesarios
only() es lo contrario de defer(): se especifican los campos a cargar y todo lo demás queda diferido. Es más explícito cuando la lista de campos necesarios es corta.
# Cargar solo lo necesario para la vista de lista
posts = Post.objects.only("id", "title", "slug", "author_id", "published_at")
SQL generado:
SELECT id, title, slug, author_id, published_at
FROM blog_post;
A diferencia de defer(), las llamadas a only() se reemplazan en lugar de acumularse:
Post.objects.only("title").only("slug", "author_id")
# → carga solo slug y author_id (title queda descartado)
Combinar only() con select_related() es habitual y funciona correctamente:
posts = Post.objects.only("id", "title", "author_id").select_related("author")
# 1 consulta con JOIN, campos mínimos, autor en caché
values_list(): cuando no se necesitan instancias del modelo
values_list() devuelve tuplas en lugar de instancias del modelo. No se instancia ningún objeto Python, lo que elimina el overhead del mapeo ORM.
# Tuplas (id, title)
Post.objects.filter(is_published=True).values_list("id", "title")
# → <QuerySet [(1, "Primer artículo"), (2, "Segundo artículo"), ...]>
# Valores escalares con flat=True (un solo campo obligatorio)
Post.objects.filter(is_published=True).values_list("id", flat=True)
# → <QuerySet [1, 2, 3, ...]>
# Named tuples para acceso por atributo
result = Post.objects.values_list("id", "title", named=True).first()
# result.title ✅
flat=True solo funciona con un único campo. Con varios campos, Django lanza un TypeError.
Casos de uso típicos: exportación CSV, recopilación de IDs para operaciones bulk, alimentación de una caché Redis. Cuando no se necesitan los métodos del modelo, values_list() es la mejor opción. Funciona bien junto con F() en values() para renombrar campos ORM.
Las relaciones se pueden recorrer directamente con la notación de doble guion bajo. Ni select_related() ni prefetch_related() son necesarios: Django genera el JOIN automáticamente, y ambos son ignorados silenciosamente con values() / values_list().
# ✅ JOIN directo, sin necesidad de select_related()
Post.objects.values_list("id", "author__username")
# → [(1, "alice"), (2, "alice"), ...]
# Ambos son superfluos aquí: Django une la tabla igualmente
Post.objects.select_related("author").values_list("id", "author__username")
Post.objects.prefetch_related("author").values_list("id", "author__username")
# Django hace el JOIN en ambos casos; el prefetch nunca se ejecuta
Cuidado con las relaciones ManyToMany o FK inversas: el JOIN automático multiplica las filas. Si un post tiene 3 tags, aparece 3 veces en el resultado.
# Post tiene una M2M con Tag
Post.objects.values_list("title", "tags__name")
# → [("Artículo A", "python"), ("Artículo A", "django"), ("Artículo A", "orm")]
# ↑ Artículo A aparece una vez por tag
# ✅ Deduplicar con distinct()
Post.objects.values_list("title", "tags__name").distinct()
Django Prefetch(): filtrar relaciones con un queryset personalizado
prefetch_related() solo carga todas las relaciones sin ningún filtro. El objeto Prefetch de Django permite pasar un queryset personalizado para filtrar, ordenar o anotar los objetos prefetcheados sin afectar a otras relaciones.
from django.db.models import Prefetch
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
body = models.TextField()
is_published = models.BooleanField(default=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
Sin Prefetch, se cargan todos los comentarios, publicados o no:
posts = Post.objects.prefetch_related("comments")
Con Prefetch, el filtro se integra directamente en la consulta de prefetch:
published_comments = Comment.objects.filter(is_published=True).order_by("-created_at")
posts = Post.objects.prefetch_related(
Prefetch("comments", queryset=published_comments)
)
SQL generado (Django añade automáticamente el filtro por IDs de posts):
-- Consulta 1: los posts
SELECT * FROM blog_post;
-- Consulta 2: solo comentarios publicados, vinculados a los posts obtenidos
SELECT * FROM blog_comment
WHERE is_published = true
AND post_id IN (1, 2, 3, ...)
ORDER BY created_at DESC;
Dentro del bucle, se usa el caché de prefetch:
for post in posts:
print(post.comments.all()) # ✅ caché de prefetch usado
print(post.comments.filter(...)) # ❌ consulta extra (caché ignorado)
Llamar a .filter() después del prefetch invalida el caché. Si se necesita un subconjunto, el filtro debe estar en el queryset del Prefetch. Es el mismo principio que select_for_update() en transacciones concurrentes: la intención debe declararse a nivel de consulta.
to_attr: almacenar el resultado en un atributo dedicado
to_attr almacena el resultado del prefetch en un atributo Python en lugar de alimentar el manager de la relación. Es necesario cuando se quieren varios prefetches sobre la misma relación, ya que dos objetos Prefetch apuntando al mismo lookup sin to_attr lanzan un ValueError.
from django.utils import timezone
from datetime import timedelta
posts = Post.objects.prefetch_related(
Prefetch(
"comments",
queryset=Comment.objects.filter(is_published=True),
to_attr="published_comments",
),
Prefetch(
"comments",
queryset=Comment.objects.filter(
created_at__gte=timezone.now() - timedelta(days=7)
),
to_attr="recent_comments",
),
)
for post in posts:
print(post.published_comments) # ✅ lista Python
print(post.recent_comments) # ✅ lista Python
print(post.comments.all()) # ⚠️ consulta extra (to_attr no alimenta el manager)
También se puede anidar select_related() dentro del queryset de un Prefetch para evitar N+1 en relaciones anidadas:
posts = Post.objects.prefetch_related(
Prefetch(
"comments",
queryset=Comment.objects.select_related("author").filter(is_published=True),
)
)
for post in posts:
for comment in post.comments.all():
print(comment.author.username) # ✅ ninguna consulta extra
¿Qué método Django ORM elegir?
| Método | Devuelve | Usar cuando |
|---|---|---|
defer(*fields) | Instancias del modelo | Excluir pocos campos pesados bien identificados |
only(*fields) | Instancias del modelo | La lista de campos necesarios es corta |
values_list() | Tuplas o escalares | No se necesitan los métodos del modelo |
Prefetch() | Instancias del modelo | Filtrar o anotar relaciones prefetcheadas |
Los cuatro se pueden combinar. Un queryset típico en una vista de lista:
published_comments = Comment.objects.filter(is_published=True).select_related("author")
posts = (
Post.objects.filter(is_published=True)
.only("id", "title", "slug", "author_id", "published_at")
.select_related("author")
.prefetch_related(Prefetch("comments", queryset=published_comments))
.order_by("-published_at")
)
Resultado: 2 consultas SQL en lugar de N+2, con únicamente las columnas necesarias.
Medir antes de optimizar con Django Debug Toolbar
Ninguno de estos métodos es útil sin medición. connection.queries y Django Debug Toolbar son las herramientas de referencia:
from django.db import connection
posts = list(Post.objects.defer("content"))
print(len(connection.queries)) # Número de consultas ejecutadas
str(queryset.query) muestra el SQL generado antes de la evaluación. Útil para verificar que defer() o only() excluye realmente las columnas esperadas, y que Prefetch() aplica el filtro directamente en la base de datos.
