Как в Django решается проблема N+1 запросов?

«Как в Django решается проблема N+1 запросов?» — вопрос из категории Django, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Проблема "N+1 запросов" возникает, когда код выполняет один запрос для получения списка объектов, а затем N дополнительных запросов для получения связанных данных для каждого из этих объектов. В Django эта проблема решается с помощью методов QuerySet: select_related и prefetch_related.

1. select_related

Используется для оптимизации доступа к связанным объектам через отношения ForeignKey и OneToOne. Он работает, выполняя SQL JOIN и включая поля связанного объекта в основной запрос.

  • Когда использовать: Для отношений "один-к-одному" и "многие-к-одному".
  • Как работает: Один SQL-запрос с JOIN.

Пример:

# Проблема: 1 запрос за книгами + N запросов за авторами
# models.py: class Book(models.Model): author = ForeignKey(Author, ...)
for book in Book.objects.all():
    print(book.author.name) # Выполняется запрос к БД на каждой итерации

# Решение: 1 запрос, который объединяет книги и авторов
for book in Book.objects.select_related('author').all():
    print(book.author.name) # Данные об авторе уже загружены

2. prefetch_related

Используется для отношений ManyToMany и обратных ForeignKey (related name). Он работает иначе: выполняет отдельный запрос для связанных объектов, а затем "склеивает" данные в Python.

  • Когда использовать: Для отношений "многие-ко-многим" и "один-ко-многим".
  • Как работает: Два отдельных SQL-запроса (один для основных объектов, второй для связанных с WHERE ... IN (...)), объединение происходит на уровне Python.

Пример:

# Проблема: 1 запрос за авторами + N запросов за книгами для каждого автора
# models.py: class Author(models.Model): ...
#            class Book(models.Model): authors = ManyToManyField(Author, ...)
for author in Author.objects.all():
    print([book.title for book in author.books.all()]) # Запрос к БД на каждой итерации

# Решение: 2 запроса вне зависимости от количества авторов
for author in Author.objects.prefetch_related('books').all():
    # Все книги для всех авторов уже загружены
    print([book.title for book in author.books.all()])

Для более сложной фильтрации предзагруженных данных можно использовать объект Prefetch.