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 tener unique=True, de lo contrario Django lanza un ValueError.

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([...])
RetornoQuerySet (lista)dict {id: instancia}
Acceso por IDO(n) — recorridoO(1) — clave directa
Consultas SQL11
IDs ausentesignorados silenciosamenteclave ausente del dict
field_namenosí (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.