Ответ
Subquery в Django ORM — это объект, который позволяет создавать вложенные SQL-запросы. Его основное преимущество — возможность выполнять сложные выборки и аннотации, которые зависят от другой таблицы, за один-единственный запрос к базе данных, избегая проблемы N+1.
Практический пример
Допустим, у нас есть модели Post и Comment, и мы хотим для каждого поста получить текст его последнего комментария.
1. Модели:
# models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
2. Запрос с Subquery:
# views.py
from django.db.models import OuterRef, Subquery
from .models import Post, Comment
# Создаем подзапрос, который для каждого поста (OuterRef('pk'))
# найдет самый новый комментарий.
latest_comment = Comment.objects.filter(
post=OuterRef('pk')
).order_by('-created_at')
# Аннотируем каждый пост текстом последнего комментария.
# Подзапрос `latest_comment.values('text')[:1]` вернет
# только одно значение — текст одного комментария.
posts_with_last_comment = Post.objects.annotate(
last_comment_text=Subquery(latest_comment.values('text')[:1])
)
# Теперь у каждого объекта `post` в `posts_with_last_comment`
# будет дополнительное поле `last_comment_text`.
Ключевые аспекты
OuterRef: Используется для ссылки на поле из внешнего запроса (в примере —pkмоделиPost).- Эффективность: Вся операция выполняется на уровне СУБД одним SQL-запросом, что гораздо производительнее, чем делать отдельные запросы для каждого объекта в цикле.
- Ограничение результата: Подзапрос, используемый для аннотации или фильтрации, должен возвращать ровно одно значение (одну строку и один столбец). Для этого часто используется срез
[:1].
Ответ 18+ 🔞
Смотри, ну вот объясняю тебе про эти Subquery в Django, как есть. Это такая штука, которая позволяет впихнуть один SQL-запрос внутрь другого, прямо как матрёшку, только полезную, а не ту, что на полке пылится. Главный профит — ты можешь за один заход, одним жирным запросом к базе, наделать таких аннотаций и выборок, что голова кругом, и при этом избежишь этой ебаной проблемы N+1, которая жрёт производительность, как голодный студент доширак.
Давай на пальцах, с примером
Представь, у тебя есть посты и комментарии к ним. И тебе вдруг приспичило для каждого поста вытащить текст самого свежего комментария, не делая отдельный запрос для каждого поста. Вот как это делается.
1. Модели (тут всё стандартно):
# models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
2. Сам запрос, где и происходит вся магия:
# views.py
from django.db.models import OuterRef, Subquery
from .models import Post, Comment
# Сначала готовим подзапрос. Он как бы говорит: "Эй, для каждого поста снаружи (OuterRef('pk')),
# найди-ка мне его комментарии и отсортируй их так, чтобы самый новый был сверху".
latest_comment = Comment.objects.filter(
post=OuterRef('pk')
).order_by('-created_at')
# А теперь основным запросом берём все посты и к каждому приклеиваем (аннотируем) результат подзапроса.
# Обрати внимание на [:1] — это чтобы подзапрос вернул одну строчку, а не кучу. Иначе СУБД обосрётся.
posts_with_last_comment = Post.objects.annotate(
last_comment_text=Subquery(latest_comment.values('text')[:1])
)
# Всё, ебать! Теперь у каждого объекта `post` в этом кверисете есть поле `last_comment_text`
# с текстом последнего комментария. И всё это — ОДНИМ запросом к базе. Красота!
На что смотреть, чтобы не облажаться
OuterRef: Это твой крючок, чтобы зацепиться за внешний запрос. Без него подзапрос не поймёт, о каком именно посте ты говоришь. Типа "эй, смотри наpkтого поста, который мы сейчас обрабатываем снаружи".- Производительность — овердохуища: Вся эта конструкция выполняется на стороне базы данных одним здоровенным SQL-запросом. Это в разы, а то и в сотни раз быстрее, чем в цикле на питоне дёргать базу для каждого поста отдельно. Серьёзно, не надо так.
- Одно значение — и точка: Подзапрос, который ты суёшь в
Subquery, должен вернуть ровно одну колонку и одну строку. Ни больше, ни меньше. Поэтому почти всегда в конце торчит этот срез[:1]. Если забудешь — получишь ошибку, и будет тебе, хитрой жопе, пиздец, а не результат.