Ответ 1
Одна из проблем вашего примера заключается в том, что вы не можете использовать queryset.count()
в качестве подзапроса, потому что .count()
пытается оценить набор запросов и вернуть счетчик.
Поэтому можно подумать, что правильным подходом было бы использовать вместо него Count()
. Может быть, что-то вроде этого:
Post.objects.annotate(
count=Count(Tag.objects.filter(post=OuterRef('pk')))
)
Это не сработает по двум причинам:
Набор запросов
Tag
выбирает все поляTag
, тогда какCount
может рассчитывать только на одно поле. Таким образом:Tag.objects.filter(post=OuterRef('pk')).only('pk')
необходим (чтобы выбрать подсчетtag.pk
).Count
сам по себе не является классомSubquery
,Count
являетсяAggregate
. Таким образом, выражение, сгенерированноеCount
, не распознается какSubquery
(OuterRef
требует подзапроса), мы можем исправить это, используяSubquery
.
Применение исправлений для 1) и 2) приведет к:
Post.objects.annotate(
count=Count(Subquery(Tag.objects.filter(post=OuterRef('pk')).only('pk')))
)
Однако если вы проверяете производимый запрос
SELECT
"tests_post"."id",
"tests_post"."title",
COUNT((SELECT U0."id"
FROM "tests_tag" U0
INNER JOIN "tests_post_tags" U1 ON (U0."id" = U1."tag_id")
WHERE U1."post_id" = ("tests_post"."id"))
) AS "count"
FROM "tests_post"
GROUP BY
"tests_post"."id",
"tests_post"."title"
Вы можете заметить, что у нас есть пункт GROUP BY
. Это связано с тем, что Count является Агрегатом, сейчас он не влияет на результат, но в некоторых других случаях может. Вот почему документы предлагают немного другой подход, в котором агрегация перемещается в subquery
посредством определенной комбинации values
+ annotate
+ values
Post.objects.annotate(
count=Subquery(
Tag.objects.filter(post=OuterRef('pk'))
# The first .values call defines our GROUP BY clause
# Its important to have a filtration on every field defined here
# Otherwise you will have more than one group per row!!!
# This will lead to subqueries to return more than one row!
# But they are not allowed to do that!
# In our example we group only by post
# and we filter by post via OuterRef
.values('post')
# Here we say: count how many rows we have per group
.annotate(count=Count('pk'))
# Here we say: return only the count
.values('count')
)
)
Наконец, это приведет к:
SELECT
"tests_post"."id",
"tests_post"."title",
(SELECT COUNT(U0."id") AS "count"
FROM "tests_tag" U0
INNER JOIN "tests_post_tags" U1 ON (U0."id" = U1."tag_id")
WHERE U1."post_id" = ("tests_post"."id")
GROUP BY U1."post_id"
) AS "count"
FROM "tests_post"