Zpět na domů

Optimalizace Django: z 30 s na 142 ms

Případ optimalizace Django monolitu: odstranění N+1, přidání indexů a zavedení DDD zkrátily čas reportů z 30 s na 142 ms. CPU DB kleslo o 60 %, dotazy – z 2800 na 3. Struktura Use Cases zrychlila vývoj funkcí.

Jak zrychlit Django reporty 200krát: případ
Advertisement 728x90

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:

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;

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.

Google AdInline article slot

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.

Google AdInline article slot

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

Advertisement 728x90

Číst dál