Делал ли subquery в Django
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Subquery в Django ORM
Делал subquery в Django много раз при разработке сложных аналитических и фильтрующих запросов. Рассмотрю различные подходы и реальные примеры.
1. Простой Subquery для фильтрации
Найти всех пользователей, которые оставили комментарий после определённой даты:
from django.db.models import OuterRef, Subquery, Exists
from django.utils import timezone
from datetime import timedelta
# Способ 1: с использованием Subquery
recent_comments = Comment.objects.filter(
user_id=OuterRef('pk'),
created_at__gte=timezone.now() - timedelta(days=7)
).values('id')
users_with_recent_comments = User.objects.annotate(
has_comment=Exists(recent_comments)
).filter(has_comment=True)
2. Subquery для получения значений
Получить последний комментарий каждого пользователя:
from django.db.models import Max, Subquery, OuterRef
# Получить дату последнего комментария
last_comment_date = Comment.objects.filter(
user_id=OuterRef('pk')
).order_by('-created_at').values('created_at')[:1]
users = User.objects.annotate(
last_comment=Subquery(last_comment_date)
).values('id', 'username', 'last_comment')
3. Exists для проверки существования записей
Найти пользователей с хотя бы одним активным постом:
from django.db.models import Exists, OuterRef, Q
active_posts = Post.objects.filter(
author_id=OuterRef('pk'),
is_published=True,
status='active'
).values('id')
users_with_posts = User.objects.filter(
Exists(active_posts)
)
4. Nested Subquery — многоуровневые запросы
Найти категории, которые имеют посты с комментариями:
from django.db.models import Exists, OuterRef, Q
# Слой 3: посты с комментариями
posts_with_comments = Comment.objects.filter(
post_id=OuterRef('pk')
).values('id')
# Слой 2: категории с нужными постами
categories = Category.objects.filter(
Exists(
Post.objects.filter(
category_id=OuterRef('pk'),
Exists(posts_with_comments)
).values('id')
)
)
5. Subquery с aggrегацией
Получить среднее количество комментариев на пост:
from django.db.models import Avg, Count, Subquery, F
# Для каждого пользователя: среднее комментариев на пост
comments_per_post = Comment.objects.filter(
post__author_id=OuterRef('pk')
).values('post__author_id').annotate(
avg_comments=Avg('id')
).values('avg_comments')
users = User.objects.annotate(
avg_comments=Subquery(comments_per_post)
).values('id', 'username', 'avg_comments')
6. Case/When с Subquery
Получить статус активности пользователя:
from django.db.models import Case, When, Value, CharField, Exists, OuterRef
from django.utils import timezone
from datetime import timedelta
recent_activity = Activity.objects.filter(
user_id=OuterRef('pk'),
timestamp__gte=timezone.now() - timedelta(hours=24)
).values('id')
users = User.objects.annotate(
activity_status=Case(
When(Exists(recent_activity), then=Value('active')),
default=Value('inactive'),
output_field=CharField()
)
).values('id', 'username', 'activity_status')
7. Реальный пример: рекомендации постов
Получить посты для рекомендации (популярные, от подписанных авторов):
from django.db.models import Count, Exists, OuterRef, F, Subquery
from django.db.models.functions import Coalesce
# Подписан ли текущий пользователь на автора
subscribed_to_author = Subscription.objects.filter(
subscriber_id=request.user.id,
author_id=OuterRef('author_id')
).values('id')
# Посты с числом лайков и статусом подписки
recommended_posts = Post.objects.filter(
is_published=True
).annotate(
likes_count=Count('likes', distinct=True),
is_from_subscribed=Exists(subscribed_to_author),
score=Coalesce(F('likes_count') * 2, 0) + Case(
When(is_from_subscribed=True, then=Value(5)),
default=Value(0)
)
).order_by('-score')[:20]
8. Сырой SQL vs ORM Subquery
Когда ORM становится сложным, можно использовать raw SQL:
from django.db.models import Subquery, F
from django.db import connection
# Вариант 1: Django ORM (сложный)
complex_query = User.objects.filter(...).annotate(...)
# Вариант 2: Сырой SQL (иногда проще)
with connection.cursor() as cursor:
cursor.execute("""
SELECT u.id, u.username, COUNT(c.id) as comment_count
FROM auth_user u
LEFT JOIN comments c ON u.id = c.user_id
GROUP BY u.id
HAVING COUNT(c.id) > 5
ORDER BY comment_count DESC
""")
results = cursor.fetchall()
9. Оптимизация Subquery
Проблема: N+1 запрос
# Плохо: для каждого пользователя дополнительный запрос
for user in User.objects.all():
comments = user.comment_set.all() # N+1 запрос!
Решение: select_related и prefetch_related
# Хорошо: один запрос с джойном
users = User.objects.prefetch_related('comment_set').all()
# Или с annotate
users = User.objects.annotate(
comment_count=Count('comment')
).values('id', 'username', 'comment_count')
10. Практический чеклист
Когда использовать Subquery:
- Фильтрация по агрегированным значениям
- Поиск записей с определённой связью
- Многоуровневые условия
- Получение дополнительных полей из связанных таблиц
Когда использовать Exists:
- Просто проверка наличия связанных записей
- Более эффективно чем Count для больших наборов
Когда писать raw SQL:
- Сложные аналитические запросы
- Запросы быстрее выполняются в SQL
- Window функции, CTE и т.д.
Заключение
Subquery в Django — мощный инструмент для сложных запросов, но требует понимания SQL и производительности. Всегда проверяй select_related и prefetch_related перед использованием Subquery, так как они часто решают проблему проще и быстрее.