Examen des performances Django
Examinez le code Django pour les problèmes de performance validés. Recherchez dans la base de code pour confirmer les problèmes avant de les signaler. Signalez uniquement ce que vous pouvez prouver.
Approche d'examen
- Recherchez d'abord - Tracez le flux de données, vérifiez les optimisations existantes, validez le volume de données
- Validez avant de signaler - La correspondance de motifs n'est pas une validation
- Zéro résultat est acceptable - Ne fabriquez pas de problèmes pour paraître rigoureux
- La sévérité doit correspondre à l'impact - Si vous vous surprenez à écrire « mineur » dans un résultat CRITIQUE, ce n'est pas critique. Rétrogradez ou ignorez-le.
Catégories d'impact
Les problèmes sont organisés par impact. Concentrez-vous sur CRITIQUE et ÉLEVÉ - ce sont eux qui causent de vrais problèmes à l'échelle.
| Priorité | Catégorie | Impact |
|---|---|---|
| 1 | Requêtes N+1 | CRITIQUE - Se multiplie avec les données, provoque des timeouts |
| 2 | Querysets non limités | CRITIQUE - Épuisement mémoire, terminations OOM |
| 3 | Index manquants | ÉLEVÉ - Analyses complètes de tables sur de grandes tables |
| 4 | Boucles d'écriture | ÉLEVÉ - Contention de verrous, requêtes lentes |
| 5 | Motifs inefficaces | FAIBLE - Rarement digne d'être signalé |
Priorité 1 : Requêtes N+1 (CRITIQUE)
Impact : Chaque N+1 ajoute O(n) allers-retours en base de données. 100 lignes = 100 requêtes supplémentaires. 10 000 lignes = timeout.
Règle : Préchargez les données connexes accédées dans les boucles
Validez en traçant : Vue → Queryset → Template/Sérialiseur → Accès à la boucle
# PROBLÈME : N+1 - chaque itération interroge le profil
def user_list(request):
users = User.objects.all()
return render(request, 'users.html', {'users': users})
# Template :
# {% for user in users %}
# {{ user.profile.bio }} ← déclenche une requête par utilisateur
# {% endfor %}
# SOLUTION : Préchargez dans la vue
def user_list(request):
users = User.objects.select_related('profile')
return render(request, 'users.html', {'users': users})
Règle : Préchargez dans les sérialiseurs, pas seulement dans les vues
Les sérialiseurs DRF qui accèdent à des champs connexes causent N+1 si le queryset n'est pas optimisé.
# PROBLÈME : SerializerMethodField requête par objet
class UserSerializer(serializers.ModelSerializer):
order_count = serializers.SerializerMethodField()
def get_order_count(self, obj):
return obj.orders.count() # ← requête par utilisateur
# SOLUTION : Annotez dans le viewset, accédez dans le sérialiseur
class UserViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return User.objects.annotate(order_count=Count('orders'))
class UserSerializer(serializers.ModelSerializer):
order_count = serializers.IntegerField(read_only=True)
Règle : Les propriétés de modèle qui interrogent sont dangereuses dans les boucles
# PROBLÈME : La propriété déclenche une requête lors de l'accès
class User(models.Model):
@property
def recent_orders(self):
return self.orders.filter(created__gte=last_week)[:5]
# Utilisé dans une boucle template = N+1
# SOLUTION : Utilisez Prefetch avec un queryset personnalisé, ou annotez
Liste de vérification de validation pour N+1
- [ ] Flux de données tracé de la vue au template/sérialiseur
- [ ] Confirmé que le champ connexe est accédé à l'intérieur d'une boucle
- [ ] Recherche dans la base de code pour select_related/prefetch_related existants
- [ ] Vérifié que la table a un nombre de lignes significatif (1000+)
- [ ] Confirmé que c'est un chemin chaud (pas admin, pas action rare)
Priorité 2 : Querysets non limités (CRITIQUE)
Impact : Charger des tables entières épuise la mémoire. Les grandes tables causent des terminations OOM et des redémarrages de workers.
Règle : Paginisez toujours les points de terminaison de liste
# PROBLÈME : Pas de pagination - charge toutes les lignes
class UserListView(ListView):
model = User
template_name = 'users.html'
# SOLUTION : Ajoutez la pagination
class UserListView(ListView):
model = User
template_name = 'users.html'
paginate_by = 25
Règle : Utilisez iterator() pour le traitement en lot à grande échelle
# PROBLÈME : Charge tous les objets en mémoire à la fois
for user in User.objects.all():
process(user)
# SOLUTION : Diffusez avec iterator()
for user in User.objects.iterator(chunk_size=1000):
process(user)
Règle : N'appelez jamais list() sur des querysets non limités
# PROBLÈME : Force l'évaluation complète en mémoire
all_users = list(User.objects.all())
# SOLUTION : Gardez en tant que queryset, découpez si nécessaire
users = User.objects.all()[:100]
Liste de vérification de validation pour Querysets non limités
- [ ] Table est grande (10k+ lignes) ou croîtra sans limite
- [ ] Pas de classe pagination, paginate_by, ou découpage
- [ ] S'exécute sur une requête accessible aux utilisateurs (pas un travail en arrière-plan avec découpage)
Priorité 3 : Index manquants (ÉLEVÉ)
Impact : Analyses complètes de tables. Négligeable sur les petites tables, catastrophique sur les grandes.
Règle : Indexez les champs utilisés dans les clauses WHERE sur les grandes tables
# PROBLÈME : Filtrage sur un champ non indexé
# User.objects.filter(email=email) # analyse complète sans index
class User(models.Model):
email = models.EmailField() # ← pas de db_index
# SOLUTION : Ajoutez un index
class User(models.Model):
email = models.EmailField(db_index=True)
Règle : Indexez les champs utilisés dans ORDER BY sur les grandes tables
# PROBLÈME : Le tri nécessite une analyse complète sans index
Order.objects.order_by('-created')
# SOLUTION : Indexez le champ de tri
class Order(models.Model):
created = models.DateTimeField(db_index=True)
Règle : Utilisez des index composés pour les motifs de requête courants
class Order(models.Model):
user = models.ForeignKey(User)
status = models.CharField(max_length=20)
created = models.DateTimeField()
class Meta:
indexes = [
models.Index(fields=['user', 'status']), # pour filter(user=x, status=y)
models.Index(fields=['status', '-created']), # pour filter(status=x).order_by('-created')
]
Liste de vérification de validation pour Index manquants
- [ ] Table a 10k+ lignes
- [ ] Champ utilisé dans filter() ou order_by() sur chemin chaud
- [ ] Vérifié le modèle - pas d'entrée db_index=True ou Meta.indexes
- [ ] Pas une clé étrangère (déjà indexée automatiquement)
Priorité 4 : Boucles d'écriture (ÉLEVÉ)
Impact : N écritures en base de données au lieu de 1. Contention de verrous. Requêtes lentes.
Règle : Utilisez bulk_create à la place de create() dans les boucles
# PROBLÈME : N insertions, N allers-retours
for item in items:
Model.objects.create(name=item['name'])
# SOLUTION : Insertion en lot unique
Model.objects.bulk_create([
Model(name=item['name']) for item in items
])
Règle : Utilisez update() ou bulk_update à la place de save() dans les boucles
# PROBLÈME : N mises à jour
for obj in queryset:
obj.status = 'done'
obj.save()
# SOLUTION A : Une seule instruction UPDATE (même valeur pour tous)
queryset.update(status='done')
# SOLUTION B : bulk_update (valeurs différentes)
for obj in objects:
obj.status = compute_status(obj)
Model.objects.bulk_update(objects, ['status'], batch_size=500)
Règle : Utilisez delete() sur le queryset, pas dans les boucles
# PROBLÈME : N suppressions
for obj in queryset:
obj.delete()
# SOLUTION : Une seule suppression
queryset.delete()
Liste de vérification de validation pour Boucles d'écriture
- [ ] Boucle itère sur 100+ éléments (ou non limité)
- [ ] Chaque itération appelle create(), save(), ou delete()
- [ ] S'exécute sur une requête accessible aux utilisateurs (pas un script de migration unique)
Priorité 5 : Motifs inefficaces (FAIBLE)
Rarement digne d'être signalé. Incluez uniquement comme notes mineures si vous signalez déjà de vrais problèmes.
Motif : count() vs exists()
# Légèrement sous-optimal
if queryset.count() > 0:
do_thing()
# Marginalement mieux
if queryset.exists():
do_thing()
Généralement ignorez - la différence est <1ms dans la plupart des cas.
Motif : len(queryset) vs count()
# Récupère toutes les lignes pour compter
if len(queryset) > 0: # mauvais si queryset pas encore évalué
# Une seule requête COUNT
if queryset.count() > 0:
Signalez uniquement si le queryset est grand et pas encore évalué.
Motif : get() dans petites boucles
# N requêtes, mais si N est petit (< 20), souvent acceptable
for id in ids:
obj = Model.objects.get(id=id)
Signalez uniquement si la boucle est grande ou si c'est dans un chemin très chaud.
Exigences de validation
Avant de signaler un QUELCONQUE problème :
- Tracez le flux de données - Suivez le queryset de sa création à sa consommation
- Recherchez les optimisations existantes - Grep pour select_related, prefetch_related, pagination
- Vérifiez le volume de données - Vérifiez si la table est vraiment grande
- Confirmez le chemin chaud - Tracez les sites d'appel, vérifiez que cela s'exécute fréquemment
- Écartez les atténuations - Vérifiez la mise en cache, le limiteur de débit
Si vous ne pouvez pas valider toutes les étapes, ne signalez pas.
Format de sortie
## Examen des performances Django : [Nom du fichier/composant]
### Résumé
Problèmes validés : X (Y Critique, Z Élevé)
### Résultats
#### [PERF-001] Requête N+1 dans UserListView (CRITIQUE)
**Localisation :** `views.py:45`
**Problème :** Champ connexe `profile` accédé dans la boucle template sans préchargement.
**Validation :**
- Tracé : UserListView → queryset users → user_list.html → `{{ user.profile.bio }}` dans la boucle
- Recherche dans la base de code : aucun select_related('profile') trouvé
- Table User : 50k+ lignes (vérifiée dans l'admin)
- Chemin chaud : lié à partir de la navigation de la page d'accueil
**Preuve :**
```python
def get_queryset(self):
return User.objects.filter(active=True) # pas de select_related
Correctif :
def get_queryset(self):
return User.objects.filter(active=True).select_related('profile')
Si aucun problème trouvé : « Aucun problème de performance identifié après examen de [fichiers] et validation de [ce que vous avez vérifiés]. »
**Avant de soumettre, vérifiez chaque résultat :**
- La sévérité correspond-elle à l'impact réel ? (« Inefficacité mineure » ≠ CRITIQUE)
- Est-ce un vrai problème de performance ou juste une préférence de style ?
- La correction de ceci améliorerait-elle mesurément les performances ?
Si la réponse à l'une d'elles est « non » - supprimez le résultat.
---
## Ce qu'il NE FAUT PAS signaler
- Fichiers de test
- Vues admin uniquement
- Commandes de gestion
- Fichiers de migration
- Scripts ponctuels
- Code derrière des feature flags désactivés
- Tables avec <1000 lignes qui ne croîtront pas
- Motifs dans les chemins froids (code rarement exécuté)
- Micro-optimisations (exists vs count, only/defer sans preuve)
### Faux positifs à éviter
**L'assignation de variable queryset n'est pas un problème :**
```python
# C'EST BON - pas de différence de performance
projects_qs = Project.objects.filter(org=org)
projects = list(projects_qs)
# vs ceci - performance identique
projects = list(Project.objects.filter(org=org))
Les querysets sont paresseux. Assigner à une variable n'exécute rien.
Les motifs de requête unique ne sont pas N+1 :
# C'EST UNE SEULE requête, pas N+1
projects = list(Project.objects.filter(org=org))
N+1 nécessite une boucle qui déclenche des requêtes supplémentaires. Un appel list() unique est correct.
Le select_related manquant sur une récupération d'objet unique n'est pas N+1 :
# Ce sont 2 requêtes, pas N+1 - signalez comme FAIBLE au maximum
state = AutofixState.objects.filter(pr_id=pr_id).first()
project_id = state.request.project_id # deuxième requête
N+1 nécessite une boucle. Un objet unique faisant 2 requêtes au lieu de 1 peut être signalé comme FAIBLE si pertinent, mais jamais comme CRITIQUE/ÉLEVÉ.
Les préférences de style ne sont pas des problèmes de performance : Si votre seule suggestion est « combinez ces deux lignes » ou « renommez cette variable » - c'est du style, pas de la performance. Ne le signalez pas.