Optimalizace Django monolitu: od 30 sekund k 142 ms u reportů
V legacy projektu na Django se reporty pro pobočky načítaly 30 sekund kvůli úplnému seq scanu tabulky orders s 280 tisíci záznamy. EXPLAIN ANALYZE ukázal:
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 generovalo 2800+ dotazů kvůli N+1 na souvisejících objektech. Byznys logika byla umístěna v kontrolerech, což zvyšovalo zátěž.
Původní dotaz:
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;
Odstranění N+1
N+1 způsoboval exponenciální růst zátěže. Původně:
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
Po úpravě s prefetch:
orders = Order.objects.filter(
branch_id=branch_id
).select_related('customer').prefetch_related('order_items__product')
Počet dotazů se snížil na 3. select_related používá JOIN pro ForeignKey, prefetch_related — IN-dotazy pro ManyToMany a zpětné vazby.
Indexace tabulek
Po odstranění N+1 zůstal seq scan. Přidány indexy bez výpadku:
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('czech', name));
Nový plán:
Index Scan using idx_orders_branch_created on orders
Index Cond: (branch_id = 42)
Execution Time: 142 ms
Doba provedení klesla z 28 s na 142 ms. CONCURRENTLY zabraňuje blokování tabulek v produkci.
Klíčová doporučení pro indexy:
- Složené na (filtr + řazení)
- GIN pro fulltextové vyhledávání
- Vždy kontrolovat EXPLAIN ANALYZE před přidáním
Refaktorování na DDD
Byznys logika v kontrolerech vytvářela technický dluh. Vyčleněny Domain a Use Cases nezávisle na frameworku.
Příklad 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 se stal tenkým:
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 se testují s mock repozitáři bez Django a databáze. Time-to-market nových funkcí se zkrátil na polovinu.
Kontrola kvality kódu
Přidáno:
- Mypy strict mode:
[mypy]
strict = true
disallow_untyped_defs = true
warn_return_any = true
- Pytest s coverage ≥87%, blokující quality gate v GitLab CI.
- MTTD snížen o 40 % díky včasnému odhalení chyb.
Metriky zlepšení
| Ukazatel | Před | Po |
|------------|------|-------|
| Doba reportu | 30 s | 1,5 s |
| CPU DB | 80 % | 32 % |
| Dotazů/stránka | 2800+ | 3 |
| TTM funkcí | X | X/2 |
| MTTD | - | -40 % |
Co je důležité
- EXPLAIN ANALYZE — první krok při jakémkoli zhoršení výkonu
- N+1 ničí pod zátěží, prefetch_related/select_related jsou povinné
- Složené indexy na (branch_id, created_at) dávají 200násobné zrychlení
- DDD izoluje byznys logiku, urychluje vývoj a testování
- Mypy + pytest + CI gate snižují MTTD bez režie
— Editorial Team
Zatím žádné komentáře.