Optimisation d'un Monolithe Django : De 30 Secondes à 142 ms pour les Rapports
Dans un projet Django hérité, les rapports de succursale prenaient 30 secondes à charger en raison d'un balayage séquentiel complet sur la table des commandes contenant 280 000 enregistrements. EXPLAIN ANALYZE a révélé :
Seq Scan on orders (cost=0.00..18420.00 rows=2841 width=847)
Filter: (branch_id = 42)
Rows Removed by Filter: 284100
Execution Time: 28340 ms
L'ORM générait plus de 2800 requêtes en raison du problème N+1 sur les objets liés. La logique métier placée dans les contrôleurs aggravait la charge.
Requête originale :
SELECT * FROM orders
JOIN order_items ON orders.id = order_items.order_id
JOIN products ON order_items.product_id = products.id
WHERE orders.branch_id = 42;
Élimination du Problème N+1
Le problème N+1 provoquait une croissance exponentielle de la charge. Avant :
orders = Order.objects.filter(branch_id=branch_id)
for order in orders:
items = order.order_items.all() # N+1
for item in items:
product = item.product # N+1
Après avec préchargement :
orders = Order.objects.filter(
branch_id=branch_id
).select_related('customer').prefetch_related('order_items__product')
Le nombre de requêtes est tombé à 3. select_related utilise une JOINTURE pour ForeignKey, prefetch_related utilise des requêtes IN pour ManyToMany et les relations inverses.
Indexation des Tables
Après le N+1, le balayage séquentiel persistait. Des index ont été ajoutés sans interruption de service :
CREATE INDEX CONCURRENTLY idx_orders_branch_created ON orders(branch_id, created_at DESC);
CREATE INDEX CONCURRENTLY idx_products_search ON products USING GIN(to_tsvector('english', name));
Nouveau plan :
Index Scan using idx_orders_branch_created on orders
Index Cond: (branch_id = 42)
Execution Time: 142 ms
Le temps d'exécution est passé de 28 secondes à 142 ms. CONCURRENTLY empêche le verrouillage des tables en production.
Recommandations clés pour l'indexation :
- Composite sur (filtre + tri)
- GIN pour la recherche en texte intégral
- Toujours vérifier EXPLAIN ANALYZE avant d'ajouter
Refactorisation vers DDD
La logique métier dans les contrôleurs créait de la dette technique. Le Domaine et les Cas d'Utilisation ont été extraits indépendamment du framework.
Exemple de Cas d'Utilisation :
class GetBranchReportUseCase:
def __init__(self, repo: OrderRepository):
self._repo = repo
def execute(self, branch_id, period) -> BranchReport:
orders = self._repo.get_by_branch_and_period(branch_id, period)
return BranchReport.from_orders(orders)
La vue est devenue légère :
class BranchReportView(APIView):
def get(self, request, branch_id):
use_case = GetBranchReportUseCase(DjangoOrderRepository())
report = use_case.execute(branch_id, DateRange.from_request(request))
return Response(BranchReportSerializer(report).data)
Les Cas d'Utilisation sont testés avec des dépôts simulés sans Django et la base de données. Le time-to-market pour les nouvelles fonctionnalités a été divisé par deux.
Contrôle de la Qualité du Code
Ajouté :
- Mode strict Mypy :
[mypy]
strict = true
disallow_untyped_defs = true
warn_return_any = true
- Pytest avec une couverture ≥87%, bloquant la passerelle qualité dans GitLab CI.
- MTTD réduit de 40% grâce à la détection précoce des erreurs.
Métriques d'Amélioration
| Métrique | Avant | Après |
|--------|--------|-------|
| Temps Rapport | 30 s | 1,5 s |
| CPU BD | 80% | 32% |
| Requêtes/Page | 2800+ | 3 |
| TTM Fonctionnalité | X | X/2 |
| MTTD | - | -40% |
Points Clés à Retenir
- EXPLAIN ANALYZE est la première étape pour toute dégradation de performance
- Le problème N+1 est fatal sous charge, prefetch_related/select_related sont obligatoires
- Les index composites sur (branch_id, created_at) offrent une accélération de 200x
- DDD isole la logique métier, accélérant le développement et les tests
- Mypy + pytest + passerelle CI réduisent le MTTD sans surcharge
— Editorial Team
Aucun commentaire pour le moment.