C’est une fonctionnalité très sophistiquée sur laquelle je suis en train de jouer et qui saigne rapidement. Je souhaite annoter un agrégat de sous-requête sur un ensemble de requêtes existant. Faire cela avant la 1.11 impliquait soit de personnaliser SQL, soit de marteler la base de données. Voici la documentation à ce sujet et son exemple:
from Django.db.models import OuterRef, Subquery, Sum
comments = Comment.objects.filter(post=OuterRef('pk')).values('post')
total_comments = comments.annotate(total=Sum('length')).values('total')
Post.objects.filter(length__gt=Subquery(total_comments))
Ils sont annotant sur l'ensemble, ce qui me semble bizarre, mais peu importe.
Je me bats avec cela, alors je le résume à l'exemple le plus simple du monde réel pour lequel j'ai des données. J'ai Carpark
s qui contient beaucoup de Space
s. Utilisation Book→Author
_ si cela vous rend plus heureux mais, pour le moment, je veux simplement commenter le nombre de modèles associés à l'aide de Subquery
*.
spaces = Space.objects.filter(carpark=OuterRef('pk')).values('carpark')
count_spaces = spaces.annotate(c=Count('*')).values('c')
Carpark.objects.annotate(space_count=Subquery(count_spaces))
Cela me donne une belle ProgrammingError: more than one row returned by a subquery used as an expression
et dans ma tête, cette erreur est parfaitement logique. La sous-requête renvoie une liste d'espaces avec le total annoté.
L'exemple suggérait qu'une sorte de magie se produirait et que je finirais avec un nombre que je pourrais utiliser. Mais ça ne se passe pas ici? Comment annoter des données de sous-requête agrégées?
J'ai construit un nouveau modèle Carpark/Space et cela a fonctionné. La prochaine étape consiste donc à déterminer ce qui empoisonne mon code SQL. Sur les conseils de Laurent, j'ai jeté un coup d'œil au code SQL et essayé de le faire ressembler davantage à la version qu'ils ont publiée dans leur réponse. Et c'est là que j'ai trouvé le vrai problème:
SELECT "bookings_carpark".*, (SELECT COUNT(U0."id") AS "c"
FROM "bookings_space" U0
WHERE U0."carpark_id" = ("bookings_carpark"."id")
GROUP BY U0."carpark_id", U0."space"
)
AS "space_count" FROM "bookings_carpark";
Je l'ai souligné, mais c'est la sous-requête GROUP BY ... U0."space"
. C'est réaccorder les deux pour une raison quelconque. Les enquêtes continuent.
Edit 2: Ok, en regardant la sous-requête SQL, je peux voir ce deuxième groupe en passant par
In [12]: print(Space.objects_standard.filter().values('carpark').annotate(c=Count('*')).values('c').query)
SELECT COUNT(*) AS "c" FROM "bookings_space" GROUP BY "bookings_space"."carpark_id", "bookings_space"."space" ORDER BY "bookings_space"."carpark_id" ASC, "bookings_space"."space" ASC
Edit 3 : D'accord! Ces deux modèles ont des ordres de tri. Celles-ci sont transmises à la sous-requête. Ce sont ces commandes qui gonflent ma requête et la cassent.
Je suppose que cela pourrait être un bug dans Django mais à moins de supprimer le Meta-order_by sur ces deux modèles, y at-il un moyen que je peux unsort une requête au moment de la requête?
* Je sais que je pourrais simplement annoter un compte pour cet exemple . Mon but réel pour utiliser ceci est un nombre de filtres beaucoup plus complexe, mais je ne peux même pas le faire fonctionner.
Il est également possible de créer une sous-classe de Subquery
, qui modifie le code SQL qu'il génère. Par exemple, vous pouvez utiliser:
class SQCount(Subquery):
template = "(SELECT count(*) FROM (%(subquery)s) _count)"
output_field = models.IntegerField()
Vous utilisez ensuite ceci comme vous le feriez avec la classe Subquery
originale:
spaces = Space.objects.filter(carpark=OuterRef('pk')).values('pk')
Carpark.objects.annotate(space_count=SQCount(spaces))
Vous pouvez utiliser cette astuce (au moins dans postgres) avec une gamme de fonctions d'agrégation: je l'utilise souvent pour constituer un tableau de valeurs, ou pour les additionner.
Shazaam! Selon mes modifications, une colonne supplémentaire était en sortie de ma sous-requête. C'était pour faciliter la commande (ce qui n'est pas nécessaire dans un COUNT).
Je devais simplement supprimer la méta-commande prescrite du modèle. Vous pouvez le faire en ajoutant simplement un .order_by()
vide à la sous-requête. Dans mon code, cela signifiait:
spaces = Space.objects.filter(carpark=OuterRef('pk')).order_by().values('carpark')
count_spaces = spaces.annotate(c=Count('*')).values('c')
Carpark.objects.annotate(space_count=Subquery(count_spaces))
Et ça marche. Superbement. Si ennuyant.
Je viens de tomber dans un cas TRÈS similaire, où je devais obtenir des réservations de sièges pour des événements où le statut de réservation n'était pas annulé. Après avoir essayé de résoudre le problème pendant des heures, voici ce que j'ai considéré comme la cause fondamentale du problème:
Préface: c'est MariaDB, Django 1.11.
Lorsque vous annotez une requête, il obtient une clause GROUP BY
Avec les champs que vous sélectionnez (en gros, ce que contient votre sélection de requête values()
). Après avoir étudié avec l'outil de ligne de commande MariaDB pourquoi j'obtiens NULL
s ou None
s sur les résultats de la requête, je suis arrivé à la conclusion que la clause GROUP BY
COUNT()
pour renvoyer NULL
s.
Ensuite, j'ai commencé à me plonger dans l'interface QuerySet
pour voir comment puis-je supprimer manuellement le forçage de GROUP BY
Des requêtes de la base de données, et voici le code suivant:
from Django.db.models.fields import PositiveIntegerField
reserved_seats_qs = SeatReservation.objects.filter(
performance=OuterRef(name='pk'), status__in=TAKEN_TYPES
).values('id').annotate(
count=Count('id')).values('count')
# Query workaround: remove GROUP BY from subquery. Test this
# vigorously!
reserved_seats_qs.query.group_by = []
performances_qs = Performance.objects.annotate(
reserved_seats=Subquery(
queryset=reserved_seats_qs,
output_field=PositiveIntegerField()))
print(performances_qs[0].reserved_seats)
Donc, fondamentalement, vous devez supprimer/mettre à jour manuellement le champ group_by
Sur le sous-requête de la sous-requête afin de ne pas y ajouter un GROUP BY
Au moment de l'exécution. De plus, vous devrez spécifier le champ de sortie de la sous-requête, car il semble que Django ne le reconnaît pas automatiquement et lève des exceptions lors de la première évaluation du jeu de requêtes. Fait intéressant, le la deuxième évaluation réussit sans elle.
Je pense que c'est un bogue Django, ou une inefficacité dans les sous-requêtes. Je vais créer un rapport de bogue à ce sujet.
Edit: le rapport de bogue est ici .
Si je comprends bien, vous essayez de compter Space
s disponible dans un Carpark
. La sous-requête semble exagérée pour cela, le bon vieil annotate seul devrait faire l'affaire:
Carpark.objects.annotate(Count('spaces'))
Cela inclura un spaces__count
valeur dans vos résultats.
OK, j'ai vu votre note ...
J'ai également pu exécuter votre même requête avec d'autres modèles que j'avais sous la main. Les résultats sont les mêmes, donc la requête de votre exemple semble être OK (testé avec Django 1.11b1):
activities = Activity.objects.filter(event=OuterRef('pk')).values('event')
count_activities = activities.annotate(c=Count('*')).values('c')
Event.objects.annotate(spaces__count=Subquery(count_activities))
Peut-être que votre "exemple le plus simple du monde réel" est trop simple ... pouvez-vous partager les modèles ou d'autres informations?
Une solution qui fonctionnerait pour n'importe quelle agrégation générale pourrait être implémentée en utilisant Window
classes de Django 2.0. J'ai ajouté cela au suivi Django billet aussi.
Cela permet d'agréger des valeurs annotées en calculant l'agrégat sur des partitions en fonction du modèle de requête externe (dans la clause GROUP BY), puis en annotant ces données sur chaque ligne du sous-ensemble de requêtes. La sous-requête peut ensuite utiliser les données agrégées de la première ligne renvoyée et ignorer les autres lignes.
Performance.objects.annotate(
reserved_seats=Subquery(
SeatReservation.objects.filter(
performance=OuterRef(name='pk'),
status__in=TAKEN_TYPES,
).annotate(
reserved_seat_count=Window(
expression=Count('pk'),
partition_by=[F('performance')]
),
).values('reserved_seat_count')[:1],
output_field=FloatField()
)
)
"travaille pour moi" n'aide pas beaucoup. Mais. J'ai essayé votre exemple sur certains modèles que j'avais à portée de main (type Book -> Author
), Cela fonctionne très bien pour moi dans Django 1.11b1.
Êtes-vous sûr de l'exécuter dans la bonne version de Django? Est-ce le code que vous utilisez actuellement? Est-ce que vous testez ceci non pas sur carpark
mais sur un modèle plus complexe?
Essayez peut-être de print(thequery.query)
pour voir quel code SQL il tente d’exécuter dans la base de données. Voici ce que j'ai obtenu avec mes modèles (édités pour répondre à votre question):
SELECT (SELECT COUNT(U0."id") AS "c"
FROM "carparks_spaces" U0
WHERE U0."carpark_id" = ("carparks_carpark"."id")
GROUP BY U0."carpark_id") AS "space_count" FROM "carparks_carpark"
Pas vraiment une réponse, mais j'espère que ça aidera.