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

Ответ

Проблема "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.

Ответ 18+ 🔞

Слушай, а вот эта ваша проблема N+1 запросов — это же классика, блядь! Прямо как в том анекдоте про Герасима и Муму, только с базой данных. Представь: ты один раз сходил в магазин за хлебом, а потом на каждый бутерброд — отдельно за колбасой, за маслом, за хуем в пальто. И так сто раз! Ну ёпта, кто ж так жить-то может?

Вот смотри, в Django есть два волшебных пенделя, которые эту хуйню лечат: select_related и prefetch_related. Они как два брата-близнеца, только один для простых связей, а второй — для сложных, с извращениями.

Первый братан — select_related. Он для ситуаций, когда у тебя одна книга — один автор (ForeignKey или OneToOne). Этот тип работает на чистом SQL-джойне. Один запрос — и всё притащил, как будто в один мешок запихнул.

# До оптимизации: 1 запрос за всеми книгами + по запросу на КАЖДОГО автора. Пиздец.
for book in Book.objects.all():
    print(book.author.name) # Каждый раз лезем в БД, ебать мои старые костыли!

# После: один жирный запрос с JOIN'ом, и всё.
for book in Book.objects.select_related('author').all():
    print(book.author.name) # Автор уже тут, в кармане, можно хоть потрогать.

Второй братан — prefetch_related. А вот это уже для разврата: много авторов на много книг (ManyToMany) или когда у одного автора куча книг (обратная связь). Он хитрее: делает два запроса. Сначала всех авторов, потом ВСЕ их книги одним махом, а потом в Питоне аккуратно раскидывает, кто кому принадлежит. Умная жопа, блядь!

# До: взяли авторов, а потом на каждого — отдельный поход за его книгами. Овердохуища запросов!
for author in Author.objects.all():
    print([book.title for book in author.books.all()]) # Снова и снова в базу...

# После: всего ДВА запроса. Хуй с горы, да?
for author in Author.objects.prefetch_related('books').all():
    # Все книги уже здесь, лежат отсортированные по авторам.
    print([book.title for book in author.books.all()])

А если тебе надо не просто все книги предзагрузить, а, например, только те, что изданы после 2000 года? Тут на сцену выходит объект Prefetch. Это как сказать: «Э, дружок-пирожок, притащи мне книги, но только не все подряд, а отфильтрованные». Хитрая жопа, но работает.

Главное — не перепутай их, а то будет как в той истории: «Муму!» — а она уже на дне. Выбирай инструмент по смыслу связи, и будет тебе счастье, а не N+1 головная боль.