Проблема N+1 в Django ORM: Ускоряем сайт в 10 раз
Автор: Javohir Abdullayev · · Django

Неожиданная проблема в реальном проекте
В проектах малого и среднего масштаба Django ORM предоставляет разработчикам огромное удобство. Без необходимости писать SQL-запросы вручную, мы легко взаимодействуем с базой данных всего несколькими строками кода на Python. Однако по мере роста аудитории проекта и увеличения объёма данных возникают неожиданные замедления.
Недавно на платформе электронной коммерции одного из наших клиентов возникла серьёзная проблема: хотя на главной странице каталога должно было отображаться всего 50 товаров, время загрузки растягивалось до 4,8 секунды. Процессор сервера (CPU) стал стабильно работать с нагрузкой в 90–95%. При анализе проблемы выяснилось, что главным виновником оказалась именно проблема запросов N+1.
Что такое проблема N+1?
Проблема N+1 возникает тогда, когда приложение отправляет 1 SQL-запрос для получения основного списка объектов, а затем ещё N отдельных дополнительных запросов для связанных данных каждого элемента из этого списка. В итоге общее количество запросов достигает N + 1.
Давайте рассмотрим на примере простой модели:
# models.py
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=100)
class Product(models.Model):
title = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag')
class Tag(models.Model):
title = models.CharField(max_length=50)Чтобы вывести список товаров в API или шаблон, разработчик пишет следующий код:
# views.py (Плохая практика)
products = Product.objects.all()[:50]
for product in products:
print(product.category.name) # На каждом шаге новый SQL-запрос!Что здесь происходит? Сначала отправляется 1 запрос для получения 50 товаров. Затем при чтении категории каждого товара Django отправляет в базу данных ещё 50 отдельных запросов. А если на странице выводятся ещё и теги, количество запросов моментально превышает 150–200.
Как мы выявили проблему?
Чтобы наглядно увидеть и подтвердить эту ситуацию, мы подключили к проекту Django Debug Toolbar. Результат оказался поразительным: для загрузки одной страницы в базу данных отправлялось 254 практически одинаковых SQL-запроса. Именно по этой причине открытие страницы занимало несколько секунд.
Решение: select_related и prefetch_related
Для подобных ситуаций Django ORM предлагает два мощных инструмента оптимизации: select_related() и prefetch_related(). Они отличаются друг от друга механизмом работы.
1. select_related() — для ForeignKey и OneToOne
select_related() использует оператор JOIN на уровне SQL. Он объединяет и извлекает основную таблицу и связанные с ней таблицы ForeignKey или OneToOneField в рамках одного единственного SQL-запроса.
# Оптимизированный вариант
products = Product.objects.select_related('category').all()[:50]
for product in products:
print(product.category.name) # Никаких дополнительных запросов к БД!Теперь вместо 50 запросов выполняется всего 1 единый SQL-запрос с JOIN.
2. prefetch_related() — для ManyToMany и обратных связей
Использование JOIN для связей «многие-ко-многим» (ManyToMany) или обратных связей ForeignKey может быть слишком ресурсоёмким. prefetch_related() решает задачу иначе: он отправляет отдельный запрос для каждой таблицы и объединяет результаты в памяти Python.
# ManyToMany и ForeignKey вместе
products = Product.objects.select_related('category').prefetch_related('tags').all()[:50]В этом случае выполняется всего 2 SQL-запроса: один для товаров и их категорий, а второй — для всех тегов, относящихся к этим товарам (с помощью условия WHERE product_id IN (...)).
3. Сложная фильтрация: объект Prefetch
Иногда требуется загрузить только активные или отсортированные связанные объекты. Для этого лучше всего использовать объект Prefetch:
from django.db.models import Prefetch
active_tags_prefetch = Prefetch(
'tags',
queryset=Tag.objects.filter(is_active=True)
)
products = Product.objects.select_related('category').prefetch_related(active_tags_prefetch)Результаты кейса
После внесения этих изменений и устранения скрытых обращений внутри шаблона мы получили в проекте следующие результаты:
- Количество SQL-запросов: сократилось с 254 до 3.
- Время загрузки страницы: снизилось с 4,8 секунды до 230 миллисекунд (ускорение более чем в 20 раз).
- Нагрузка на сервер (CPU): в пиковые часы снизилась с 90% до 15–20%.
- Пользовательский опыт: каталог товаров стал открываться практически мгновенно.
Заключение
Django ORM — удобный инструмент, однако отсутствие понимания того, какой SQL-код он генерирует, может сильно утяжелить проект. Если в вашем проекте страницы тоже работают медленно, в первую очередь проверьте количество SQL-запросов. Грамотное применение методов select_related и prefetch_related позволит вам в разы увеличить производительность приложения.
Теги: #Django #Django ORM #Backend Optimallashtirish #Python