Что такое проблема N+1 запроса в ORM и как ее решить

Ответ

Проблема N+1 запроса — это распространенная проблема производительности в ORM, когда для извлечения родительской сущности и связанных с ней дочерних сущностей выполняется один (1) запрос для родителя, а затем N дополнительных запросов (по одному для каждой дочерней сущности).

Это происходит из-за механизма "ленивой загрузки" (lazy loading), когда связанные данные подгружаются только в момент обращения к ним.

Пример проблемы (Django ORM)

# models.py
# class Author(models.Model): ...
# class Book(models.Model):
#     author = models.ForeignKey(Author, ...)

# 1 запрос для получения всех книг
books = Book.objects.all()

# В цикле для каждой из N книг выполняется еще один запрос к БД
# для получения имени автора. Итого: 1 + N запросов.
for book in books:
    print(f'"{book.title}" by {book.author.name}')

Решение: Жадная загрузка (Eager Loading)

Решение заключается в том, чтобы загрузить все необходимые данные одним или двумя оптимизированными запросами.

  1. select_related: Используется для связей ForeignKey и OneToOne. Он "расширяет" основной запрос с помощью JOIN, получая данные из связанных таблиц за один раз.

    # Выполняется 1 SQL-запрос с JOIN
    books = Book.objects.select_related('author').all()
    for book in books:
        # Данные об авторе уже загружены, дополнительный запрос не нужен
        print(f'"{book.title}" by {book.author.name}')
  2. prefetch_related: Используется для связей ManyToManyField и обратных ForeignKey. Он выполняет отдельный запрос для связанных данных и "склеивает" их в Python. Всего выполняется два запроса, что всё равно эффективнее, чем N+1.

    # 1 запрос для авторов + 1 запрос для всех их книг
    authors = Author.objects.prefetch_related('book_set').all()

Игнорирование этой проблемы может привести к катастрофическому падению производительности приложения при работе с большими объемами данных.