Optimierung eines Django-Monolithen: Von 30 Sekunden auf 142 ms bei Berichten
In einem veralteten Django-Projekt benötigten Filialberichte 30 Sekunden zum Laden, verursacht durch einen vollständigen sequenziellen Scan der Bestellungstabelle mit 280.000 Datensätzen. EXPLAIN ANALYZE zeigte:
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
Das ORM generierte über 2800 Abfragen aufgrund von N+1 bei verknüpften Objekten. Geschäftslogik in Controllern verstärkte die Last.
Ursprüngliche Abfrage:
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;
Beseitigung von N+1
N+1 verursachte exponentielles Lastwachstum. Vorher:
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
Nachher mit Prefetch:
orders = Order.objects.filter(
branch_id=branch_id
).select_related('customer').prefetch_related('order_items__product')
Abfrageanzahl sank auf 3. select_related nutzt JOIN für ForeignKey, prefetch_related verwendet IN-Abfragen für ManyToMany und Reverse-Relations.
Indizierung von Tabellen
Nach N+1 blieb der sequenzielle Scan. Indizes wurden ohne Downtime hinzugefügt:
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));
Neuer Plan:
Index Scan using idx_orders_branch_created on orders
Index Cond: (branch_id = 42)
Execution Time: 142 ms
Ausführungszeit sank von 28 Sekunden auf 142 ms. CONCURRENTLY verhindert Tabellensperren im Produktivbetrieb.
Wichtige Indizierungsempfehlungen:
- Zusammengesetzte Indizes auf (Filter + Sortierung)
- GIN für Volltextsuche
- Immer EXPLAIN ANALYZE vor dem Hinzufügen prüfen
Refactoring zu DDD
Geschäftslogik in Controllern erzeugte technische Schulden. Domain und Use Cases wurden unabhängig vom Framework extrahiert.
Beispiel-Use-Case:
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)
View wurde schlank:
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)
Use Cases werden mit Mock-Repositories ohne Django und Datenbank getestet. Time-to-Market für neue Features halbierte sich.
Code-Qualitätskontrolle
Hinzugefügt:
- Mypy strict mode:
[mypy]
strict = true
disallow_untyped_defs = true
warn_return_any = true
- Pytest mit Coverage ≥87%, blockierende Quality Gate in GitLab CI.
- MTTD sank um 40% durch frühe Fehlererkennung.
Verbesserungsmetriken
| Metrik | Vorher | Nachher |
|--------|--------|---------|
| Berichtszeit | 30 s | 1,5 s |
| DB CPU | 80% | 32% |
| Abfragen/Seite | 2800+ | 3 |
| Feature TTM | X | X/2 |
| MTTD | - | -40% |
Wichtige Erkenntnisse
- EXPLAIN ANALYZE ist der erste Schritt bei Performance-Problemen
- N+1 zerstört Performance unter Last, prefetch_related/select_related sind Pflicht
- Zusammengesetzte Indizes auf (branch_id, created_at) bringen 200-fache Beschleunigung
- DDD isoliert Geschäftslogik, beschleunigt Entwicklung und Tests
- Mypy + pytest + CI Gate reduzieren MTTD ohne Overhead
— Editorial Team
Noch keine Kommentare.