web-dev-qa-db-fra.com

Agrégation d'une annotation dans GROUP BY dans Django

[~ # ~] 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.

Le problème

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 

Requête Django

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.

La raison de l'erreur

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.

La question

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.

24
Leonid Shifrin

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')
16
Alexandr Tatarinov

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!).

3
user2485594

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é.

2
Wilfried

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.

1
Thulasi Ram