Quand on a une liste d’identifiants et qu’on veut récupérer les instances correspondantes, le réflexe habituel en Django c’est filter(pk__in=[...]). Ça marche, c’est une seule requête SQL. Mais in_bulk() est une optimisation ORM souvent ignorée : elle retourne un dictionnaire {id: instance} au lieu d’un QuerySet, ce qui change radicalement la façon d’accéder aux résultats. Là où filter() force un parcours O(n) pour retrouver un objet par son ID, in_bulk() donne un accès direct O(1).

Signature Django in_bulk() et comportement

QuerySet.in_bulk(id_list=(), *, field_name='pk')
  • id_list : liste d’identifiants à récupérer. Si omis (appelé sans argument), retourne tous les objets de la table.
  • field_name : champ utilisé comme clé du dictionnaire. Doit obligatoirement avoir unique=True, sinon Django lève une ValueError.

La requête SQL générée est une simple clause WHERE pk IN (...), une seule requête peu importe la taille de la liste.

in_bulk() vs filter() : accès O(1) au lieu de O(n)

# filter() classique → QuerySet, accès O(n)
contrats: list[Contrat] = list(Contrat.objects.filter(pk__in=[1, 2, 3]))
contrat: Contrat | None = next((c for c in contrats if c.pk == 2), None)

# in_bulk() → dict, accès O(1)
contrats_map: dict[int, Contrat] = Contrat.objects.in_bulk([1, 2, 3])
# → {1: <Contrat pk=1>, 2: <Contrat pk=2>, 3: <Contrat pk=3>}

contrat = contrats_map.get(2)  # accès direct, None si absent

Les IDs absents en base n’apparaissent tout simplement pas dans le dictionnaire retourné. Pas d’erreur, pas de None : clé manquante = objet inexistant.

in_bulk() avec field_name : indexer par n’importe quel champ unique

in_bulk() accepte n’importe quel champ unique=True via field_name :

# Par référence unique
refs: list[str] = ['REF-001', 'REF-002', 'REF-003']
contrats_map: dict[str, Contrat] = Contrat.objects.in_bulk(
    refs,
    field_name='reference'
)
# → {'REF-001': <Contrat ...>, 'REF-002': <Contrat ...>, ...}

contrat: Contrat | None = contrats_map.get('REF-002')

C’est particulièrement utile lors de synchronisations de données où l’identifiant métier n’est pas la PK.

Cas d’usage Django : quand in_bulk() fait la différence

Hydrater plusieurs agrégats en une requête

Dans un contexte DDD, quand on doit charger plusieurs agrégats à partir d’une liste d’IDs :

ids: list[int] = [event.contrat_id for event in evenements]
contrats_map: dict[int, Contrat] = Contrat.objects.in_bulk(ids)

for evenement in evenements:
    contrat: Contrat | None = contrats_map.get(evenement.contrat_id)
    if contrat:
        contrat.appliquer(evenement)

Une seule requête pour tous les contrats, puis accès direct par ID dans la boucle.

Éviter le N+1 lors d’imports

from decimal import Decimal

def importer_lignes(lignes_csv: list[dict[str, str]]) -> None:
    references: list[str] = [ligne['ref'] for ligne in lignes_csv]
    existants: dict[str, Produit] = Produit.objects.in_bulk(
        references, field_name='reference'
    )

    a_creer: list[Produit] = []
    a_mettre_a_jour: list[Produit] = []

    for ligne in lignes_csv:
        if ligne['ref'] in existants:
            produit = existants[ligne['ref']]
            produit.prix = Decimal(ligne['prix'])
            a_mettre_a_jour.append(produit)
        else:
            a_creer.append(Produit(reference=ligne['ref'], prix=ligne['prix']))

    Produit.objects.bulk_create(a_creer)
    Produit.objects.bulk_update(a_mettre_a_jour, ['prix'])

Pattern classique import/synchro : une requête in_bulk(), puis bulk_create + bulk_update. Zéro N+1.

Récupérer tous les objets d’une table

# Charge toute la table en mémoire — à réserver aux petites tables
config: dict[int, ParametreApp] = ParametreApp.objects.in_bulk()
valeur: str = config[42].valeur

Pratique pour les tables de référence (pays, devises, paramètres) qu’on consulte souvent.

Optimiser in_bulk() sur de grandes listes avec le chunking

Pour une liste de plusieurs milliers d’IDs, la clause IN(...) peut devenir lourde côté base. La solution : découper en lots.

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


# Usage
contrats: dict[int, Contrat] = in_bulk_chunked(
    Contrat.objects, liste_de_5000_ids
)

Récapitulatif : in_bulk() vs filter() Django

filter(pk__in=[...])in_bulk([...])
RetourQuerySet (liste)dict {id: instance}
Accès par IDO(n) — parcoursO(1) — clé directe
Requêtes SQL11
IDs absentsignorés silencieusementclé absente du dict
field_namenonoui (unique=True requis)

in_bulk() n’est pas un remplacement universel de filter(). C’est un outil spécifique : quand on a des IDs et qu’on veut un accès direct par clé, c’est le bon choix. Pour tout le reste, filter() reste parfaitement adapté.


Tu travailles sur des sujets de performance Django ? Jette un œil à pourquoi l’IA rend l’apprentissage du code plus essentiel que jamais.