L'un des problèmes avec votre exemple est que vous ne pouvez pas utiliser queryset.count()
en tant que sous-requête, car .count()
essaie d'évaluer le jeu de requêtes et renvoie le nombre.
On peut donc penser que la bonne approche serait d'utiliser Count()
Au lieu. Peut-être quelque chose comme ça :
Post.objects.annotate(
count=Count(Tag.objects.filter(post=OuterRef('pk')))
)
Cela ne fonctionnera pas pour deux raisons :
-
La
Tag
queryset sélectionne tous lesTag
champs, tandis queCount
ne peut compter que sur un champ. Ainsi :Tag.objects.filter(post=OuterRef('pk')).only('pk')
est nécessaire (sélectionner compter surtag.pk
). -
Count
elle-même n'est pas uneSubquery
classe,Count
est unAggregate
. Donc l'expression générée parCount
n'est pas reconnu comme uneSubquery
(OuterRef
nécessite une sous-requête), nous pouvons résoudre ce problème en utilisantSubquery
.
L'application des correctifs pour 1) et 2) produirait :
Post.objects.annotate(
count=Count(Subquery(Tag.objects.filter(post=OuterRef('pk')).only('pk')))
)
Cependant si vous inspectez la requête en cours de production :
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"
vous remarquerez un GROUP BY
clause. C'est parce que COUNT
est une fonction d'agrégation. À l'heure actuelle, cela n'affecte pas le résultat, mais dans d'autres cas, cela peut arriver. C'est pourquoi la documentation
suggérer une approche différente, où l'agrégation est déplacée dans la Subquery
via une combinaison spécifique 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')
)
)
Cela produira finalement :
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"