Приведите пример сложного QuerySet в Django с использованием аннотаций и подзапросов

Ответ

Сложные 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 наворотить, что сама база данных от тебя сбежит.