Монолит с отчётами на 30 секунд: как переписали архитектуру и что из этого вышло
Разбор начался с EXPLAIN ANALYZE: отчёт по филиалу делал Seq Scan по таблице orders с 280 тысячами записей, ORM порождал N+1, а одна страница отправляла в базу 2800+ запросов. После замены обычных обращений к связанным объектам на select_related и prefetch_related число запросов сократилось до трёх, а затем составной индекс по branch_id и created_at и GIN-индекс для полнотекстового поиска убрали полный скан и перевели запрос на Index Scan.
Индекс создавали с CONCURRENTLY, чтобы не блокировать таблицу в продакшене. На этом фоне время выполнения конкретного запроса упало с 28 секунд до 142 миллисекунд, а итоговое время открытия отчёта снизилось с 30 секунд до 1,5 секунды; CPU базы опустился с 80% до 32%.
После починки производительности автор вынес бизнес-логику из Django view в Domain и Use Cases, чтобы тестировать сценарии без HTTP, базы и самого фреймворка. Сверху добавили mypy strict, Pytest с порогом покрытия 87% и блокирующий quality gate в GitLab CI: новые фичи стали выходить вдвое быстрее, а MTTD снизился на 40%.
Коротко
- EXPLAIN ANALYZE показал полный Seq Scan по orders, 284100 строк мимо фильтра и время выполнения около 28 секунд для отчёта по филиалу.
- После перехода на select_related и prefetch_related страница отчёта вместо 2800+ запросов к базе стала делать только 3 запроса.
- Составной индекс по branch_id и created_at DESC и GIN-индекс для поиска по product name перевели запрос на Index Scan без полного сканирования.
- Индекс создавали через CONCURRENTLY, чтобы не блокировать таблицу в продакшене и не получить простой на базе с сотнями тысяч записей.
- Вынос логики в Domain и Use Cases, плюс Mypy, Pytest и quality gate в GitLab CI сократили TTM новых фич вдвое и снизили MTTD на 40%.
FAQ
Зачем в таком Django-монолите было не только ускорять SQL, но и отдельно выносить бизнес-логику из контроллеров в Domain и Use Cases?
Одной оптимизации запросов было мало: логика в контроллерах продолжала ломать старые части системы при каждом изменении. Вынос в Domain и Use Cases позволил тестировать сценарии отдельно от Django, базы и HTTP.
Почему автор сначала смотрел EXPLAIN ANALYZE, а уже потом правил ORM и добавлял индексы, вместо того чтобы просто индексировать таблицы наугад?
План запроса показал конкретные узкие места: полный скан, объём лишних строк и реальное время выполнения. После этого стало понятно, где проблема в N+1, а где нужен составной индекс.
Какой практический эффект дали quality gate, strict-типизация и тесты после рефакторинга, если основные проблемы вроде бы уже были сняты на уровне базы?
Эти проверки стали страховкой от новых регрессий. Код без прошедших тестов и нужного покрытия больше не попадал в продакшен, а ошибки начали ловиться до запуска.
Читайте также
- EXPLAIN ANALYZE как первый шаг при жалобах на медленные отчёты: При разборе проблем с производительностью сначала нужно смотреть реальный план выполнения запроса, а не сразу добавлять индексы или переписывать код. EXPLAIN ANALYZE показывает, где именно возникает узкое место: полный скан таблицы, лишние строки после фильтрации, дорогие JOIN'ы или неудачный порядок операций.
[Производительность БД]
Зарегистрированные пользователи видят только два тезиса.
Зарегистрироваться

В Django-монолите отчёты по филиалам открывались до 30 секунд из-за N+1, полного сканирования таблицы orders и логики, размазанной по контроллерам. Автор последовательно сократил нагрузку на базу, перестроил архитектуру и добавил quality gate для продакшена.