Работая с Django я долго думал, что post.author.name - это магия одного SQL запроса с JOIN, где-то там под капотом. На самом деле, оказалось не все так однозначно. По умолчанию Django ORM ленивая, каждое такое обращение это отдельный SELECT запрос к БД. Условно говоря, если в цикле пройтись по 100 постам, дернув у каждого .author.name, получится не один а 101 запрос. В этом и кроется основная проблема N+1
JOIN появляется только когда мы сами просим через select_related.
posts (id, title, author_id)
authors (id, name)for posts in Post.objects.all(): # Пример N + 1 запрос
print(post.author.name)
for post in Post.objects.select_related('author').all(): # 1 запрос с select_related
print(post.author.name)Под капотом select_related('author') выглядит так:
-- INNER JOIN: посты где есть автор
SELECT posts.title, authors.name
FROM posts
JOIN authors ON posts.author_id = authors.id;Если автора нет(когда author_id NULL) и мы не хотим терять такие посты, то нужен LEFT JOIN:
SELECT post.title, authors.name
FROM posts
LEFT JOIN authors ON posts.author_id = authors.id;Разница простая: JOIN оставляет совпавшие строки, LEFT JOIN не теряет левую сторону и ставит пусто туда, где нет пары.