Как найти и исправить проблему N+1 запросов в Django ORM?
- Чем select_related отличается от prefetch_related?
N+1 — один запрос за списком и по запросу на каждый элемент при обращении к связанному объекту в цикле. select_related — JOIN в одном запросе для ForeignKey и OneToOne. prefetch_related — отдельный запрос на связь и склейка в Python для ManyToMany и обратных связей.
- Найти: django-debug-toolbar, логирование SQL, assertNumQueries в тестах
- only/defer — не тянуть лишние колонки
- Prefetch(queryset=...) — отфильтрованная предзагрузка
А ещё Агрегаты считать в базе: annotate(Count(...)), а не len() в цикле
Подробно
Откуда берётся. Ленивые связи ORM: order.customer загружается при первом обращении. В цикле по 100 заказам — 101 запрос. В шаблонах и сериализаторах DRF это особенно незаметно.
select_related строит SQL JOIN — подходит для связей «к одному». Для «ко многим» JOIN размножил бы строки, поэтому там prefetch_related: второй запрос WHERE order_id IN (...) и раскладка результатов по объектам в памяти.
Контроль в тестах:
def test_list(self):
with self.assertNumQueries(2):
self.client.get("/orders/")
Тот же класс проблем есть в любом ORM: в Hibernate — fetch join и EntityGraph, в SQLAlchemy — joinedload и selectinload.
Пример
# N+1: 1 запрос за заказами + по запросу за покупателем на каждый
for order in Order.objects.all():
print(order.customer.name)
# 1 запрос с JOIN
for order in Order.objects.select_related("customer"):
print(order.customer.name)
# 2 запроса: заказы и все их позиции
orders = Order.objects.prefetch_related("items")Что хотят услышать
- Что такое N+1 и откуда.
- select_related и prefetch_related — когда какой.
- Как обнаружить и не допустить снова.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✗prefetch_related делает JOIN для ManyToMany, а select_related — отдельный запрос на каждую связь
- ✗N+1 решается только переходом на чистый SQL
- ✓select_related — JOIN для ForeignKey и OneToOne, prefetch_related — отдельный запрос для M2M
- ✗N+1 возникает, только если в таблице больше миллиона строк
Разбор темы
- Веб на Python — WSGI и ASGI, FastAPI, HTTP-клиенты, JSON, Django ORM
Чем WSGI отличается от ASGI, простой API на FastAPI и внедрение зависимостей, requests и httpx с таймаутами, JSON с датами и деньгами, N+1 в Django.
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.