[~ # ~] mise à jour [~ # ~]
Grâce à la réponse publiée, j'ai trouvé un moyen beaucoup plus simple de formuler le problème. La question d'origine peut être vue dans l'historique des révisions.
J'essaie de traduire une requête SQL en Django, mais j'obtiens une erreur que je ne comprends pas.
Voici le modèle Django que j'ai):
class Title(models.Model):
title_id = models.CharField(primary_key=True, max_length=12)
title = models.CharField(max_length=80)
publisher = models.CharField(max_length=100)
price = models.DecimalField(decimal_places=2, blank=True, null=True)
J'ai les données suivantes:
publisher title_id price title
--------------------------- ---------- ------- -----------------------------------
New Age Books PS2106 7 Life Without Fear
New Age Books PS2091 10.95 Is Anger the Enemy?
New Age Books BU2075 2.99 You Can Combat Computer Stress!
New Age Books TC7777 14.99 Sushi, Anyone?
Binnet & Hardley MC3021 2.99 The Gourmet Microwave
Binnet & Hardley MC2222 19.99 Silicon Valley Gastronomic Treats
Algodata Infosystems PC1035 22.95 But Is It User Friendly?
Algodata Infosystems BU1032 19.99 The Busy Executive's Database Guide
Algodata Infosystems PC8888 20 Secrets of Silicon Valley
Voici ce que je veux faire: introduire un champ annoté dbl_price
Qui est le double du prix, puis grouper le jeu de requêtes résultant par publisher
, et pour chaque éditeur, calculer le total de tous dbl_price
Valeurs pour tous les titres publiés par cet éditeur.
La requête SQL qui le fait est la suivante:
SELECT SUM(dbl_price) AS total_dbl_price, publisher
FROM (
SELECT price * 2 AS dbl_price, publisher
FROM title
) AS A
GROUP BY publisher
La sortie souhaitée serait:
publisher tot_dbl_prices
--------------------------- --------------
Algodata Infosystems 125.88
Binnet & Hardley 45.96
New Age Books 71.86
La requête ressemblerait à:
Title.objects
.annotate(dbl_price=2*F('price'))
.values('publisher')
.annotate(tot_dbl_prices=Sum('dbl_price'))
mais donne une erreur:
KeyError: 'dbl_price'.
ce qui indique qu'il ne trouve pas le champ dbl_price
dans l'ensemble de requêtes.
Voici pourquoi cette erreur se produit: la documentation dit
Vous devez également noter que average_rating a été explicitement inclus dans la liste des valeurs à renvoyer. Cela est nécessaire en raison de l'ordre des clauses values () et annotate ().
Si la clause values () précède la clause annotate (), toutes les annotations seront automatiquement ajoutées au jeu de résultats. Cependant, si la clause values () est appliquée après la clause annotate (), vous devez inclure explicitement la colonne d'agrégation.
Ainsi, dbl_price
Est introuvable dans l'agrégation, car il a été créé par un annotate
antérieur, mais n'a pas été inclus dans values()
.
Cependant, je ne peux pas non plus l'inclure dans values
, car je veux utiliser values
(suivi d'un autre annotate
) comme périphérique de regroupement, car
Si la clause values () précède l'annotate (), l'annotation sera calculée en utilisant le groupement décrit par la clause values ().
qui est la base de la façon dont Django implémente SQL GROUP BY
. Cela signifie que je ne peux pas inclure dbl_price
dans values()
, car alors le regroupement sera basé sur des combinaisons uniques des deux champs publisher
et dbl_price
, alors que je dois regrouper par publisher
uniquement.
Ainsi, la requête suivante, qui ne diffère que de la précédente en ce que j'agrège sur le champ price
du modèle plutôt que sur le champ dbl_price
Annoté, fonctionne réellement:
Title.objects
.annotate(dbl_price=2*F('price'))
.values('publisher')
.annotate(sum_of_prices=Count('price'))
car le champ price
se trouve dans le modèle plutôt que d'être un champ annoté, et nous n'avons donc pas besoin de l'inclure dans values
pour le conserver dans l'ensemble de requêtes.
Donc, nous l'avons ici: j'ai besoin d'inclure une propriété annotée dans values
pour la conserver dans l'ensemble de requêtes, mais je ne peux pas le faire parce que values
est également utilisé pour le regroupement (qui sera mal avec un champ supplémentaire). Le problème est essentiellement dû aux deux façons très différentes dont values
est utilisé dans Django, selon le contexte (que values
soit ou non suivi de annotate
) - qui est (1) extraction de valeur (SQL plain SELECT
list) et (2) groupement + agrégation sur les groupes (SQL GROUP BY
) - et dans ce cas, ces deux façons semblent en conflit.
Ma question est : y a-t-il un moyen de résoudre ce problème (sans des choses comme revenir au SQL brut)?
Veuillez noter: l'exemple spécifique en question peut être résolu en déplaçant toutes les instructions annotate
après values
, ce qui a été noté par plusieurs réponses. Cependant, je suis plus intéressé par les solutions (ou discussions) qui conserveraient les instructions annotate
avant values()
, pour trois raisons: 1. Il existe également des exemples plus complexes, où le solution de contournement suggérée ne fonctionnerait pas. 2. Je peux imaginer des situations, où l'ensemble de requêtes annoté a été passé à une autre fonction, qui fait réellement GROUP BY, de sorte que la seule chose que nous connaissons est l'ensemble des noms des champs annotés et leurs types. 3. La situation semble assez simple, et cela me surprendrait si ce choc de deux utilisations distinctes de values()
n'a pas été remarqué et discuté auparavant.
C'est peut-être un peu trop tard, mais j'ai trouvé la solution (testée avec Django 1.11.1).
Le problème est que l'appel à .values('publisher')
, qui est nécessaire pour fournir le regroupement, supprime toutes les annotations, qui ne sont pas incluses dans les champs .values()
param.
Et nous ne pouvons pas inclure dbl_price
Dans les champs param, car cela ajoutera une autre instruction GROUP BY
.
La solution consiste à effectuer toutes les agrégations, qui nécessitent d'abord des champs annotés, puis appelez .values()
et incluez ces agrégations aux champs param (this n'ajoutera pas GROUP BY
, car ce sont des agrégations). Ensuite, nous devrions appeler .annotate()
avec N'IMPORTE QUELLE expression - cela fera Django ajouter l'instruction GROUP BY
À la requête SQL en utilisant le seul champ de non-agrégation de la requête - éditeur .
Title.objects
.annotate(dbl_price=2*F('price'))
.annotate(sum_of_prices=Sum('dbl_price'))
.values('publisher', 'sum_of_prices')
.annotate(titles_count=Count('id'))
Le seul inconvénient de cette approche - si vous n'avez pas besoin d'autres agrégations sauf celle avec un champ annoté - vous devrez en inclure de toute façon. Sans le dernier appel à .annotate () (et il doit inclure au moins une expression!), Django n'ajoutera pas GROUP BY
À la requête SQL. Une approche pour y faire face est juste pour créer une copie de votre champ:
Title.objects
.annotate(dbl_price=2*F('price'))
.annotate(_sum_of_prices=Sum('dbl_price')) # note the underscore!
.values('publisher', '_sum_of_prices')
.annotate(sum_of_prices=F('_sum_of_prices')
Notez également que vous devez être prudent avec la commande QuerySet. Vous feriez mieux d'appeler .order_by()
soit sans paramètres pour effacer l'ordre, soit avec votre champ GROUP BY
. Si la requête résultante contiendra un classement par tout autre champ, le regroupement sera incorrect. https://docs.djangoproject.com/en/1.11/topics/db/aggregation/#interaction-with-default-ordering-or-order-by
En outre, vous souhaiterez peut-être supprimer cette fausse annotation de votre sortie, appelez donc à nouveau .values (). Ainsi, le code final ressemble à ceci:
Title.objects
.annotate(dbl_price=2*F('price'))
.annotate(_sum_of_prices=Sum('dbl_price'))
.values('publisher', '_sum_of_prices')
.annotate(sum_of_prices=F('_sum_of_prices')
.values('publisher', 'sum_of_prices')
.order_by('publisher')
Cela est attendu de la façon dont group_by fonctionne dans Django. Tous les champs annotés sont ajoutés dans GROUP BY
clause. Cependant, je ne peux pas expliquer pourquoi il a été rédigé de cette façon.
Vous pouvez faire fonctionner votre requête comme ceci:
Title.objects
.values('publisher')
.annotate(total_dbl_price=Sum(2*F('price'))
qui produit le SQL suivant:
SELECT publisher, SUM((2 * price)) AS total_dbl_price
FROM title
GROUP BY publisher
qui se trouve juste fonctionner dans votre cas.
Je comprends que ce n'est peut-être pas la solution complète que vous cherchiez, mais certaines annotations, même complexes, peuvent également être intégrées dans cette solution en utilisant CombinedExpressions (j'espère!).
Votre problème vient de values()
suivi de annotate()
. L'ordre est important. Ceci est expliqué dans la documentation sur [ordre des clauses d'annotation et de valeurs] ( https://docs.djangoproject.com/en/1.10/topics/db/aggregation/#order-of-annotate-and-values- clauses )
.values('pub_id')
limite le champ ensemble de requêtes avec pub_id
. Vous ne pouvez donc pas annoter sur income
La méthode values () prend des arguments positionnels facultatifs, * champs, qui spécifient les noms de champ auxquels le SELECT doit être limité.
Cette solution de @alexandr y répond correctement.
https://stackoverflow.com/a/44915227/6323666
Vous avez besoin de ceci:
from Django.db.models import Sum
Title.objects.values('publisher').annotate(tot_dbl_prices=2*Sum('price'))
Idéalement, j'ai inversé le scénario ici en les résumant d'abord, puis en le doublant. Vous essayez de doubler puis de résumer. J'espère que ça va.