Какие основные способы выполнения JOIN-запросов существуют в Django ORM?

Ответ

В Django ORM SQL-операция JOIN выполняется неявно при обращении к связанным моделям. Для оптимизации запросов и решения проблемы "N+1" существуют два ключевых метода:

  1. *`select_related(fields)** Этот метод "подтягивает" связанные объекты в одном SQL-запросе, используяLEFT OUTER JOIN. Он эффективен для связей "один-к-одному" (OneToOneField) и "многие-к-одному" (ForeignKey`).

    Проблема (N+1 запросов):

    # models.py
    # class Book(models.Model):
    #     author = models.ForeignKey(Author, on_delete=models.CASCADE)
    
    # views.py
    books = Book.objects.all() # 1-й запрос
    for book in books: 
        # На каждой итерации выполняется дополнительный запрос к таблице авторов
        print(book.author.name) # N дополнительных запросов

    Решение с select_related:

    # Выполняется один SQL-запрос с JOIN
    books = Book.objects.select_related('author').all()
    for book in books:
        # Данные об авторе уже загружены, дополнительный запрос не нужен
        print(book.author.name)
  2. *`prefetch_related(lookups)** Этот метод работает иначе: он выполняет отдельный запрос для связанных объектов и "соединяет" их в Python. Идеально подходит для связей "многие-ко-многим" (ManyToManyField) и обратных связейForeignKey`.

    Пример:

    # models.py
    # class Author(models.Model): ...
    # class Book(models.Model):
    #     authors = models.ManyToManyField(Author)
    
    # views.py
    # Получаем все книги и для каждой книги всех ее авторов
    books = Book.objects.prefetch_related('authors').all()
    for book in books:
        # authors.all() не генерирует новый запрос к БД
        author_names = [author.name for author in book.authors.all()]
        print(f"{book.title}: {', '.join(author_names)}")
  3. Неявный JOIN через filter() и exclude() Django автоматически создает JOIN, когда вы фильтруете по полям связанной модели, используя двойное подчеркивание __.

    # Найти все книги, написанные автором с именем 'Leo Tolstoy'
    # SQL: SELECT ... FROM book INNER JOIN author ON ... WHERE author.name = 'Leo Tolstoy'
    books = Book.objects.filter(author__name='Leo Tolstoy')

Сводка:

  • select_related: для ForeignKey и OneToOneField (один SQL-запрос с JOIN).
  • prefetch_related: для ManyToManyField и обратных связей (отдельный запрос и JOIN в Python).
  • filter(): для фильтрации по связанным полям (неявный JOIN).

Ответ 18+ 🔞

А, слушай, про эти джойны в Джанго! Ну, это же просто пиздец, как часто народ на грабли наступает, а потом удивляется, почему приложение тормозит, как черепаха в патоках. Сейчас разжую, как бабушка манную кашу.

Представь, у тебя есть модели: Книга и Автор. Книга ссылается на автора через ForeignKey. И вот ты такой пишешь:

books = Book.objects.all() # Всё окей, один запрос
for book in books:
    print(book.author.name) # ОПА! А вот тут-то и начинается пиздец!

Что происходит? Первый запрос за книги. А потом на КАЖДОЙ, блядь, итерации цикла — ещё один отдельный запрос в базу, чтобы достать имя автора этой конкретной книги. 100 книг = 101 запрос. Это и есть тот самый "N+1 запрос", от которого у любого нормального бэкендера волосы дыбом встают. Удивление пиздец, да?

Спаситель №1: select_related Это твой волшебный пинок под зад для связей "один-к-одному" или "многие-к-одному" (то бишь ForeignKey). Он говорит ORM: "Эй, чувак, когда будешь тащить книги, сразу одним махом, одним SQL-запросом, прихвати и данные об авторе, через LEFT OUTER JOIN".

# Вжух! И всё в одном запросе. Никакой лишней возни.
books = Book.objects.select_related('author').all()
for book in books:
    print(book.author.name) # Всё уже тут, в оперативке. Быстро, как хуй с горы.

Спаситель №2: prefetch_related А вот это уже для более сложных отношений — "многие-ко-многим" (ManyToManyField) или когда нужно "в обратную сторону" по связи пойти. Он работает хитрее: делает два (или больше) отдельных запроса, а потом уже в коде на Python аккуратно склеивает их, как хитрая жопа.

Допустим, у книги может быть несколько авторов:

# models.py
# class Book(models.Model):
#     authors = models.ManyToManyField(Author)

# views.py
# Сначала запрос за всеми книгами, потом ОТДЕЛЬНЫЙ запрос за ВСЕМИ авторами этих книг.
books = Book.objects.prefetch_related('authors').all()
for book in books:
    # И тут магия: book.authors.all() НЕ лезет в базу снова!
    author_names = [author.name for author in book.authors.all()]
    print(f"{book.title}: {', '.join(author_names)}")

А что с фильтрацией? А тут Джанго сам всё понимает, ёпта! Хочешь найти все книги Толстого? Фильтруй по полю связанной модели, и ORM сам неявно джойн нарисует.

# SQL под капотом будет с INNER JOIN. Всё честно.
books = Book.objects.filter(author__name='Лев Толстой')

Итог, чтобы не ебать мозг:

  • select_related — твой друг для ForeignKey/OneToOne. Тянет всё в одном запросе. Используй, если знаешь, что будешь лазить по связанным полям.
  • prefetch_related — твой друг для ManyToManyField и обратных связей. Делает отдельный запрос, но потом в Python всё собирает. Сложнее, но без него никуда.
  • Фильтруй через __ — и Джанго сам джойн сделает там, где надо.

Главное — не забывай про эти методы, а то накроешься медным тазом с производительностью. Волнение ебать, когда в логах видишь тысячу одинаковых запросов на одной странице.