Ответ
Сложные QuerySet в Django строятся с помощью комбинации методов для агрегации, аннотации и выполнения подзапросов, что позволяет получить нужные данные за один запрос к базе данных, избегая проблемы N+1.
Пример: Получим все посты, аннотируем их количеством активных комментариев и датой последнего активного комментария. Также сразу подгрузим связанные данные об авторе и отфильтрованные комментарии.
from django.db.models import Count, Q, Subquery, OuterRef, Prefetch
from blog.models import Post, Comment
# Подзапрос для получения даты последнего активного комментария
last_comment_subquery = Comment.objects.filter(
post=OuterRef('pk'),
is_active=True
).order_by('-created_at').values('created_at')[:1]
# Основной запрос
posts = Post.objects.annotate(
# 1. Аннотация: добавляем вычисляемое поле с количеством
active_comments_count=Count('comments', filter=Q(comments__is_active=True)),
# 2. Аннотация с подзапросом
last_comment_date=Subquery(last_comment_subquery)
).select_related('author') # 3. Оптимизация: JOIN для получения автора
.prefetch_related(
# 4. Оптимизация: отдельный запрос для получения только активных комментариев
Prefetch('comments', queryset=Comment.objects.filter(is_active=True), to_attr='active_comments')
).order_by('-last_comment_date')
Ключевые компоненты этого запроса:
annotate()иCount(): Добавляют к каждому объектуPostвычисляемое полеactive_comments_count.SubqueryиOuterRef: Выполняют вложенный запрос для каждого поста, чтобы найти дату последнего комментария.select_related('author'): Избегает дополнительных запросов к БД для получения данных автора (используетJOIN).prefetch_related()сPrefetch: Эффективно загружает связанные активные комментарии в отдельном запросе, решая проблему N+1.
Ответ 18+ 🔞
Слушай, а вот эти твои Django ORM запросы, которые на три экрана растягиваются — это ж пиздец какой-то, а не код. Ну ладно, сидишь ты, такой, думаешь: «Ща я базу данных одним запросом порешаю, как бог». А на деле получается этакая хитрая жопа из аннотаций, подзапросов и префетчей, что сам через полчаса в нём нихуя не понимаешь.
Но если разобраться, то штука мощная, ёпта. Вот смотри, представь: тебе надо все посты вытащить, но чтобы сразу с ними приехало: сколько у поста активных комментов, когда последний коммент был, кто автор, и сами комменты, но только активные. И всё это за ОДИН, блядь, запрос, а не как какой-нибудь распиздяй, который в цикле N+1 запросов гоняет.
Вот, держи, как это делают не лохи:
from django.db.models import Count, Q, Subquery, OuterRef, Prefetch
from blog.models import Post, Comment
# Подзапрос для получения даты последнего активного комментария
last_comment_subquery = Comment.objects.filter(
post=OuterRef('pk'),
is_active=True
).order_by('-created_at').values('created_at')[:1]
# Основной запрос
posts = Post.objects.annotate(
# 1. Аннотация: добавляем вычисляемое поле с количеством
active_comments_count=Count('comments', filter=Q(comments__is_active=True)),
# 2. Аннотация с подзапросом
last_comment_date=Subquery(last_comment_subquery)
).select_related('author') # 3. Оптимизация: JOIN для получения автора
.prefetch_related(
# 4. Оптимизация: отдельный запрос для получения только активных комментариев
Prefetch('comments', queryset=Comment.objects.filter(is_active=True), to_attr='active_comments')
).order_by('-last_comment_date')
И что мы тут, блядь, наворотили? А вот что, разбирайся:
annotate()сCount(): Это как прикрутить к каждому посту новый атрибут,active_comments_count. Типа «эй, Пост, вот тебе цифра, сколько у тебя живых комментов». И всё это прямо в SQL считается, а не в Питоне циклом.SubqueryиOuterRef: Это вообще магия, блядь. Для каждого поста (вот этотOuterRef('pk')— это и есть наш пост) запускается маленький внутренний запрос: «Найди-ка мне дату последнего активного комментария к этой штуке». И результат лепится к посту какlast_comment_date. Хуй с горы, да?select_related('author'): Это чтобы не быть идиотом. Без этого Django за каждым постом полезет в базу отдельно спрашивать: «А кто его автор?». А с этим — один красивый JOIN, и автор уже прицеплен. Экономия — ебать её в сраку.prefetch_related()сPrefetch: А это, сука, второй уровень дзена. Мы говорим: «Не грузи ВСЕ комменты, а только те, у которыхis_active=True». И складывай их не в стандартныйpost.comments, а в отдельный атрибутpost.active_comments. Чистота, порядок, и опять нихуя лишних запросов.
Вот и весь секрет, блядь. Выглядит страшно, но работает как швейцарские часы. Главное — не переборщить, а то можно такую муду с Subquery наворотить, что сама база данных от тебя сбежит.