Um dos problemas com seu exemplo é que você não pode usar
queryset.count()
como uma subconsulta, porque .count()
tenta avaliar o conjunto de consultas e retornar a contagem. Então, pode-se pensar que a abordagem correta seria usar
Count()
em vez de. Talvez algo assim:Post.objects.annotate(
count=Count(Tag.objects.filter(post=OuterRef('pk')))
)
Isso não funcionará por dois motivos:
-
ATag
queryset seleciona todas asTag
campos, enquantoCount
só pode contar com um campo. Assim:Tag.objects.filter(post=OuterRef('pk')).only('pk')
énecessário (para seleccionar a contagem emtag.pk
).
-
Count
em si não é umaSubquery
classe,Count
é umAggregate
. Portanto, a expressão gerada porCount
não é reconhecido como umaSubquery
(OuterRef
requer subconsulta), podemos corrigir isso usandoSubquery
.
A aplicação de correções para 1) e 2) produziria:
Post.objects.annotate(
count=Count(Subquery(Tag.objects.filter(post=OuterRef('pk')).only('pk')))
)
No entanto se você inspecionar a consulta que está sendo produzida:
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"
você notará um
GROUP BY
cláusula. Isso ocorre porque COUNT
é uma função agregada. No momento, isso não afeta o resultado, mas em alguns outros casos pode. É por isso que os documentos
sugerir uma abordagem diferente, onde a agregação é movida para a subquery
por meio de uma combinação específica de 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')
)
)
Finalmente, isso produzirá:
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"