Powrót do strony głównej

Optymalizacja Django: od 30s do 142ms

Studium przypadku optymalizacji monolitu Django: eliminacja N+1, dodanie indeksów i wdrożenie DDD skróciło czas raportów z 30 s do 142 ms. CPU BD spadło o 60%, zapytania — z 2800 do 3. Struktura Use Cases przyspieszyła rozwój funkcji.

Jak przyspieszyć raporty Django 200 razy: case
Advertisement 728x90

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:

Google AdInline article slot
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.

Google AdInline article slot

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.

Google AdInline article slot

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

Advertisement 728x90

Czytaj dalej