Profiling, bottleneck analysis, database optimization, CDN, scaling strategies
Оптимизация держится на измерениях, а не на догадках. Типичная ошибка новичка — «ускорять» код, который и так не тормозит, и заодно усложнять его без всякого выигрыша. Рабочий цикл один: измерить → найти настоящее узкое место → исправить → измерить снова.
💡 Правило (Кнут): «Преждевременная оптимизация — корень всех зол». Сначала профилирование, потом оптимизация. Без цифр вы оптимизируете не то.
| Инструмент | Что показывает | Когда |
|---|---|---|
| Django Debug Toolbar | Все SQL-запросы страницы, их время, дубли | Локальная разработка |
| django-silk | Профайлинг запросов в реальном времени, в т.ч. на стейдже | Поиск медленных эндпоинтов |
| cProfile + snakeviz | Где тратится CPU в Python-коде | CPU-bound участки |
QuerySet.explain() | План выполнения запроса в БД | Анализ конкретного SQL |
| APM (Sentry/New Relic) | Latency по эндпоинтам в production | Что реально медленно у пользователей |
pip install django-silk # /silk/ — дашборд по запросам
python -m cProfile -o out.prof manage.py <command>
snakeviz out.prof # интерактивная визуализация💡 Профилируйте на данных, похожих на боевые по объёму. Запрос без индекса на 100 строках мгновенен, а на 10 млн — кладёт сервер. Локальная пустая БД скрывает реальные проблемы.
В 90% случаев медленный Django-эндпоинт — это не Python, а запросы к БД. Порядок разбора — от самого частого к редкому.
Чаще всего производительность убивает именно это: один запрос на список плюс по отдельному запросу на каждый связанный объект.
# ❌ N+1: 1 запрос на posts + по запросу на author каждого поста
for post in Post.objects.all():
print(post.author.name) # отдельный SELECT на каждой итерации!
# ✅ select_related: JOIN, один запрос (для FK / OneToOne)
for post in Post.objects.select_related('author'):
print(post.author.name)
# ✅ prefetch_related: отдельный запрос + склейка в Python (для M2M / обратных FK)
for post in Post.objects.prefetch_related('tags', 'comments'):
print(post.tags.all())| Инструмент | Для каких связей | Механизм |
|---|---|---|
select_related | ForeignKey, OneToOne (вперёд) | SQL JOIN, один запрос |
prefetch_related | ManyToMany, обратный FK | Отдельный запрос + джойн в Python |
# values()/values_list() — не строим объекты модели, тянем только колонки
ids = Post.objects.filter(is_published=True).values_list('id', flat=True)
# only()/defer() — частичная загрузка полей (не тянуть тяжёлый TextField в списке)
Post.objects.only('id', 'title', 'created_at')
# Агрегация на стороне БД, а не в Python
from django.db.models import Count
Author.objects.annotate(post_count=Count('posts')) # вместо len(author.posts.all())Запрос фильтрует/сортирует по полю без индекса → полный скан таблицы.
class Order(models.Model):
class Meta:
indexes = [
models.Index(fields=['status', '-created_at']), # под частый фильтр+сортировку
]# Подтверждаем планом выполнения, что индекс используется (а не Seq Scan)
print(Order.objects.filter(status='paid').order_by('-created_at').explain(analyze=True))⚠️ Индексы ускоряют чтение, но замедляют запись и занимают место. Не индексируйте всё подряд — только поля, по которым реально фильтруете/сортируете/джойните.
Самый быстрый запрос — тот, который не выполнился. Кэш применяют слоями, от грубого к точечному.
# 1. Кэш целой страницы
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def article_list(request): ...
# 2. Кэш фрагмента шаблона
# {% load cache %}
# {% cache 600 sidebar request.user.id %} ... {% endcache %}
# 3. Низкоуровневый кэш — самый гибкий
from django.core.cache import cache
def get_top_products():
data = cache.get('top_products')
if data is None:
data = list(Product.objects.top()) # дорогой запрос
cache.set('top_products', data, 300)
return data⚠️ Инвалидация — самая сложная часть кэширования. Решите заранее: кэш истекает по времени (TTL) или сбрасывается по событию (сигнал
post_save). Устаревшие данные в кэше — источник коварных багов «у меня же исправлено, а на сайте старое».
💡 В production кэш-бэкенд — Redis, общий для всех воркеров.
LocMemCacheживёт в памяти одного процесса, и у каждого воркера Gunicorn будет свой несогласованный кэш.
Когда БД уже оптимизирована, профилируйте CPU.
# Генераторы и iterator() — не держим всё в памяти
for row in Order.objects.filter(year=2025).iterator(chunk_size=2000):
process(row)
# Тяжёлый расчёт в запросе → вынести из горячего пути в Celery
generate_report.delay(report_id)💡 Для CPU-bound задач (обработка изображений, экспорт больших отчётов) async и кэш не помогут — выносите в фоновые воркеры (Celery) или масштабируйте процессы.
django-storages + S3 + CloudFront): браузер берёт ассеты с ближайшего узла, минуя ваш сервер.CONN_MAX_AGE/PgBouncer) — не открывать соединение на каждый запрос.# settings.py
STATIC_URL = 'https://cdn.example.com/static/'
STORAGES = {'staticfiles': {'BACKEND': 'storages.backends.s3.S3Storage'}}select_related/prefetch_related, индексы, values().⚠️ Не начинайте с пункта 6. Добавить воркеров/серверов к коду с N+1 — значит дороже платить за ту же неэффективность.
select_related/prefetch_relatedvalues, only, агрегация в БД)explain()Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Реальный Проект: SaaS Платформа