Optymalizacja monolitu Django: od 30 sekund do 142 ms w raportach
W legacy-projekcie Django raporty dla oddziałów ładowały się 30 sekund z powodu pełnego seq scan na tabeli orders z 280 tys. rekordów. EXPLAIN ANALYZE wykazał:
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
ORM generował 2800+ zapytań z powodu N+1 na powiązanych obiektach. Logika biznesowa znajdowała się w kontrolerach, pogarszając obciążenie.
Oryginalne zapytanie:
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;
Eliminacja N+1
N+1 powodował wykładniczy wzrost obciążenia. Było:
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
Stało się z prefetch:
orders = Order.objects.filter(
branch_id=branch_id
).select_related('customer').prefetch_related('order_items__product')
Liczba zapytań spadła do 3. select_related używa JOIN dla ForeignKey, prefetch_related — zapytań IN dla ManyToMany i relacji odwrotnych.
Indeksowanie tabel
Po N+1 seq scan pozostał. Dodano indeksy bez przestojów:
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('polish', name));
Nowy plan:
Index Scan using idx_orders_branch_created on orders
Index Cond: (branch_id = 42)
Execution Time: 142 ms
Czas wykonania spadł z 28 s do 142 ms. CONCURRENTLY zapobiega blokowaniu tabel w produkcji.
Kluczowe zalecenia dotyczące indeksów:
- Złożone na (filtr + sortowanie)
- GIN dla wyszukiwania pełnotekstowego
- Zawsze sprawdzać EXPLAIN ANALYZE przed dodaniem
Refaktoryzacja w DDD
Logika biznesowa w kontrolerach tworzyła dług techniczny. Wyodrębniono Domain i Use Cases niezależnie od frameworka.
Przykład 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 stał się cienki:
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 testuje się z mock-repozytoriami bez Django i bazy danych. Time-to-market nowych funkcji skrócił się o połowę.
Kontrola jakości kodu
Dodano:
- Mypy strict mode:
[mypy]
strict = true
disallow_untyped_defs = true
warn_return_any = true
- Pytest z coverage ≥87%, blokujący quality gate w GitLab CI.
- MTTD spadł o 40% dzięki wczesnemu wykrywaniu błędów.
Metryki ulepszeń
| Wskaźnik | Było | Stało się |
|------------|------|-------|
| Czas raportu | 30 s | 1.5 s |
| CPU bazy danych | 80% | 32% |
| Zapytań/strona | 2800+ | 3 |
| TTM funkcji | X | X/2 |
| MTTD | - | -40% |
Co jest ważne
- EXPLAIN ANALYZE — pierwszy krok przy każdej degradacji wydajności
- N+1 zabija pod obciążeniem, prefetch_related/select_related są obowiązkowe
- Złożone indeksy na (branch_id, created_at) dają x200 przyspieszenie
- DDD izoluje logikę biznesową, przyspiesza rozwój i testowanie
- Mypy + pytest + CI gate redukują MTTD bez overhead
— Editorial Team
Brak komentarzy.