Базы данных

Проблема N+1 запросов и как её лечить

7 мин чтения · SQL, Django ORM

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 там, где точно понадобятся связанные объекты.

← Ко всем статьям