Uno de los problemas con su ejemplo es que no puede usar queryset.count() como una subconsulta, porque .count() intenta evaluar el conjunto de consultas y devolver el recuento.
Entonces uno puede pensar que el enfoque correcto sería usar Count() en cambio. Tal vez algo como esto:
Post.objects.annotate(
count=Count(Tag.objects.filter(post=OuterRef('pk')))
)
Esto no funcionará por dos razones:
-
La
Tagqueryset selecciona todas lasTagcampos, mientras queCountsólo puede contar con un campo. Así:Tag.objects.filter(post=OuterRef('pk')).only('pk')es necesario (para seleccionar contar contag.pk). -
Counten sí mismo no es unaSubqueryclase,Countes unAggregate. Entonces la expresión generada porCountno se reconoce como unaSubquery(OuterRefrequiere subconsulta), podemos arreglar eso usandoSubquery.
La aplicación de correcciones para 1) y 2) produciría:
Post.objects.annotate(
count=Count(Subquery(Tag.objects.filter(post=OuterRef('pk')).only('pk')))
)
Sin embargo si inspecciona la consulta que se está produciendo:
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"
notará un GROUP BY cláusula. Esto se debe a que COUNT es una función agregada. Ahora mismo no afecta al resultado, pero en algunos otros casos puede que sí. Es por eso que los docs
sugiera un enfoque diferente, donde la agregación se mueva a la subquery a través de una combinación 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 esto producirá:
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"