Cuando tenemos una lista de identificadores y queremos recuperar las instancias correspondientes, el reflejo habitual en Django es filter(pk__in=[...]). Funciona — una sola consulta SQL. Pero in_bulk() es una optimización ORM frecuentemente ignorada: retorna un diccionario {id: instancia} en lugar de un QuerySet, lo que cambia radicalmente la forma de acceder a los resultados. Donde filter() obliga a un recorrido O(n) para encontrar un objeto por su ID, in_bulk() da acceso directo O(1).
Firma de in_bulk() y comportamiento
QuerySet.in_bulk(id_list=(), *, field_name='pk')
id_list: lista de identificadores a recuperar. Si se omite (llamado sin argumentos), retorna todos los objetos de la tabla.field_name: campo usado como clave del diccionario. Debe tenerunique=True, de lo contrario Django lanza unValueError.
La consulta SQL generada es una simple cláusula WHERE pk IN (...) — una sola consulta independientemente del tamaño de la lista.
in_bulk() vs filter(): acceso O(1) en lugar de O(n)
# filter() → QuerySet, acceso O(n)
contratos: list[Contrato] = list(Contrato.objects.filter(pk__in=[1, 2, 3]))
contrato: Contrato | None = next((c for c in contratos if c.pk == 2), None)
# in_bulk() → dict, acceso O(1)
contratos_map: dict[int, Contrato] = Contrato.objects.in_bulk([1, 2, 3])
# → {1: <Contrato pk=1>, 2: <Contrato pk=2>, 3: <Contrato pk=3>}
contrato = contratos_map.get(2) # acceso directo, None si ausente
Los IDs ausentes en base de datos simplemente no aparecen en el diccionario retornado. Sin error, sin None: clave ausente = objeto inexistente.
in_bulk() con field_name: indexar por cualquier campo único
in_bulk() acepta cualquier campo unique=True mediante field_name:
# Por referencia única
refs: list[str] = ['REF-001', 'REF-002', 'REF-003']
contratos_map: dict[str, Contrato] = Contrato.objects.in_bulk(
refs,
field_name='referencia'
)
# → {'REF-001': <Contrato ...>, 'REF-002': <Contrato ...>, ...}
contrato: Contrato | None = contratos_map.get('REF-002')
Especialmente útil en sincronizaciones de datos donde el identificador de negocio no es la PK.
Casos de uso Django: cuándo in_bulk() marca la diferencia
Hidratar varios agregados en una consulta
En un contexto DDD, cuando hay que cargar varios agregados a partir de una lista de IDs:
ids: list[int] = [evento.contrato_id for evento in eventos]
contratos_map: dict[int, Contrato] = Contrato.objects.in_bulk(ids)
for evento in eventos:
contrato: Contrato | None = contratos_map.get(evento.contrato_id)
if contrato:
contrato.aplicar(evento)
Una sola consulta para todos los contratos, luego acceso directo por ID en el bucle.
Evitar el N+1 durante imports
from decimal import Decimal
def importar_filas(filas_csv: list[dict[str, str]]) -> None:
referencias: list[str] = [fila['ref'] for fila in filas_csv]
existentes: dict[str, Producto] = Producto.objects.in_bulk(
referencias, field_name='referencia'
)
a_crear: list[Producto] = []
a_actualizar: list[Producto] = []
for fila in filas_csv:
if fila['ref'] in existentes:
producto = existentes[fila['ref']]
producto.precio = Decimal(fila['precio'])
a_actualizar.append(producto)
else:
a_crear.append(Producto(referencia=fila['ref'], precio=fila['precio']))
Producto.objects.bulk_create(a_crear)
Producto.objects.bulk_update(a_actualizar, ['precio'])
Patrón clásico import/sincronización: una consulta in_bulk(), luego bulk_create + bulk_update. Cero N+1.
Recuperar todos los objetos de una tabla
# Carga toda la tabla en memoria — reservar para tablas pequeñas
config: dict[int, ParametroApp] = ParametroApp.objects.in_bulk()
valor: str = config[42].valor
Práctico para tablas de referencia (países, divisas, parámetros) consultadas frecuentemente.
Optimizar in_bulk() en listas grandes con chunking
Para listas de miles de IDs, la cláusula IN(...) puede volverse pesada para la base de datos. La solución: dividir en lotes.
from collections.abc import Iterator
from itertools import islice
from typing import Any
from django.db.models import QuerySet
def chunked(iterable: list[Any], size: int) -> Iterator[list[Any]]:
it = iter(iterable)
while chunk := list(islice(it, size)):
yield chunk
def in_bulk_chunked(
queryset: QuerySet,
ids: list[Any],
chunk_size: int = 500,
field_name: str = 'pk',
) -> dict[Any, Any]:
result: dict[Any, Any] = {}
for chunk in chunked(ids, chunk_size):
result.update(queryset.in_bulk(chunk, field_name=field_name))
return result
# Uso
contratos: dict[int, Contrato] = in_bulk_chunked(
Contrato.objects, lista_de_5000_ids
)
Resumen: in_bulk() vs filter() en Django
filter(pk__in=[...]) | in_bulk([...]) | |
|---|---|---|
| Retorno | QuerySet (lista) | dict {id: instancia} |
| Acceso por ID | O(n) — recorrido | O(1) — clave directa |
| Consultas SQL | 1 | 1 |
| IDs ausentes | ignorados silenciosamente | clave ausente del dict |
field_name | no | sí (unique=True requerido) |
in_bulk() no es un reemplazo universal de filter(). Es una herramienta específica: cuando se tienen IDs y se quiere acceso directo por clave, es la elección correcta. Para todo lo demás, filter() sigue siendo perfectamente adecuado.
¿Trabajas en temas de rendimiento Django? Echa un vistazo a por qué la IA hace que aprender a programar sea más esencial que nunca.