Приведите пример сложного 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.