N+1 — одна из самых частых причин, почему страница со списком «внезапно» начинает загружаться секундами вместо миллисекунд. Проблема в том, что ORM незаметно для разработчика превращает один логический запрос в десятки или сотни отдельных SQL-запросов.
Как это возникает
Допустим, есть список статей, и для каждой нужно вывести имя автора:
articles = Article.objects.all() # 1 запрос: получить все статьи
for article in articles:
print(article.title, "—", article.author.name) # +1 запрос НА КАЖДУЮ статью
Если статей 50, ORM выполнит 1 запрос на получение статей и ещё 50 отдельных запросов — по одному на каждого автора, потому что связанный объект author подгружается лениво (lazy loading), в момент первого обращения к нему.
Как это увидеть
В Django удобно включить логирование запросов или воспользоваться django-debug-toolbar, которая прямо в браузере показывает количество и текст всех SQL-запросов на странице.
from django.db import connection
# ... код, который выполняет запросы ...
print(len(connection.queries)) # если тут 51 вместо 1 — это и есть N+1
Решение: eager loading
Вместо того чтобы подгружать связанные объекты по одному, можно попросить ORM забрать их заранее — одним дополнительным запросом (или через JOIN).
# Для связи "один-к-одному" / "многие-к-одному" — JOIN одним запросом
articles = Article.objects.select_related("author")
# Для связи "многие-ко-многим" / "один-ко-многим" — отдельный запрос + склейка в Python
articles = Article.objects.prefetch_related("tags")
for article in articles:
print(article.title, "—", article.author.name) # авторы уже в памяти, доп. запросов нет
| Метод | Когда использовать | Как работает |
|---|---|---|
select_related |
ForeignKey, OneToOne | Один SQL-запрос с JOIN |
prefetch_related |
ManyToMany, обратный ForeignKey | Доп. запрос + склейка в Python |
Итог
N+1 почти никогда не заметен на маленьких тестовых данных — 5 статей дадут 6 запросов, и это выглядит нормально. Проблема всплывает в проде на реальных объёмах данных. Поэтому имеет смысл с самого начала привыкать проверять количество запросов на страницах со списками и заранее продумывать select_related/prefetch_related там, где точно понадобятся связанные объекты.