Агрегирование аннотации в GROUP BY в Django

UPDATE

Благодаря опубликованному ответу, я нашел гораздо более простой способ сформулировать проблему. Оригинальный вопрос можно увидеть в истории изменений.

Проблема

Я пытаюсь перевести SQL-запрос в Django, но получаю ошибку, которую я не понимаю.

Вот модель Django у меня:

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)

У меня есть следующие данные:

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   Database Guide
Algodata Infosystems         PC8888        20     Secrets of Silicon Valley

Вот что я хочу сделать: введите аннотированное поле dbl_price, которое в два раза больше цены, затем сгруппируйте полученный запрос на publisher и для каждого издателя вычислите общее количество всех значений dbl_price для всех названия, опубликованные этим издателем.

Запрос SQL, который делает это, выглядит следующим образом:

SELECT SUM(dbl_price) AS total_dbl_price, publisher
FROM (
  SELECT price * 2 AS dbl_price, publisher
  FROM title
) AS A 
GROUP BY publisher

Желаемый результат:

publisher                    tot_dbl_prices
---------------------------  --------------
Algodata Infosystems                 125.88
Binnet & Hardley                      45.96
New Age Books                         71.86 

Запрос Django

Запрос будет выглядеть так:

Title.objects
 .annotate(dbl_price=2*F('price'))
 .values('publisher')
 .annotate(tot_dbl_prices=Sum('dbl_price'))

но дает ошибку:

KeyError: 'dbl_price'. 

который указывает, что он не может найти поле dbl_price в запросе.

Причина ошибки

Вот почему эта ошибка происходит: в документации говорится

Вы также должны заметить, что в среднем      в списке возвращаемых значений. Это необходимо из-за упорядочивания предложений values ​​() и annotate().

Если предложение values ​​() предшествует предложению annotate(), любые аннотации      будет автоматически добавлен в результирующий набор. Однако, если      values ​​() применяется после предложения annotate(), вам нужно явно указать столбец агрегата.

Итак, dbl_price не удалось найти в агрегации, потому что он был создан предыдущим annotate, но не был включен в values().

Однако я не могу включить его в values, потому что я хочу использовать values (за которым следует другой annotate) в качестве группирующего устройства, поскольку

Если предложение values ​​() предшествует аннотации(), аннотация будет вычисляться с использованием группировки, описываемой предложением values ​​().

который является основой того, как Django реализует SQL GROUP BY. Это означает, что я не могу включить dbl_price внутри values(), потому что тогда группировка будет основана на уникальных комбинациях обоих полей publisher и dbl_price, тогда как мне нужно группировать только publisher.

Таким образом, следующий запрос, который отличается от вышеприведенного в том, что я агрегировал над полем модели price вместо аннотированного поля dbl_price, фактически работает:

Title.objects
 .annotate(dbl_price=2*F('price'))
 .values('publisher')
 .annotate(sum_of_prices=Count('price'))

потому что поле price находится в модели, а не является аннотированным полем, поэтому нам не нужно включать его в values, чтобы сохранить его в наборе запросов.

Вопрос

Итак, вот оно: мне нужно включить аннотированное свойство в values, чтобы сохранить его в наборе запросов, но я не могу этого сделать, потому что values также используется для группировки (что будет неправильно с дополнительное поле). Проблема в основном связана с двумя очень разными способами, в которых values используется в Django, в зависимости от контекста (следует ли за values следовать annotate) - который является (1) извлечением значения (SQL plain SELECT) и (2) группировка + агрегация по группам (SQL GROUP BY) - и в этом случае эти два способа конфликтуют.

Мой вопрос: есть ли способ решить эту проблему (без таких вещей, как возврат к необработанному sql)?

Обратите внимание: конкретный пример может быть решен путем перемещения всех операторов annotate после values, что было отмечено несколькими ответами. Однако меня больше интересуют решения (или обсуждения), которые сохраняли бы инструкции annotate до values() по трем причинам: 1. Существуют также более сложные примеры, где предлагаемое обходное решение не будет работать. 2. Я могу представить ситуации, когда аннотированный запрос был передан другой функции, которая фактически выполняет GROUP BY, так что единственное, что мы знаем, это набор имен аннотированных полей и их типов. 3. Ситуация кажется довольно простой, и это меня удивило бы, если бы это столкновение двух разных видов использования values() не было замечено и обсуждалось ранее.

Ответы

Ответ 1

Возможно, это слишком поздно, но я нашел решение (протестировано с Django 1.11.1).

Проблема в том, что вызов .values('publisher'), который требуется для обеспечения группировки, удаляет все аннотации, которые не включены в param полей .values().

И мы не можем включить dbl_price в поля param, потому что это добавит еще один оператор GROUP BY.

Решение .values() в том, чтобы выполнить все агрегации, для которых сначала требуются аннотированные поля, затем вызвать .values() и включить эти агрегации в поля param (это не добавит GROUP BY, потому что они являются агрегатами). Затем мы должны вызвать .annotate() с .annotate() выражением - это заставит django добавить оператор GROUP BY к запросу SQL, используя единственное поле неагрегирования в запросе - издатель.

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'))

Единственный минус в этом подходе - если вам не нужны никакие другие агрегаты, кроме той, которая содержит аннотированное поле, - вам все равно придется включить некоторые. Без последнего вызова .annotate() (и он должен включать хотя бы одно выражение!), Django не добавит GROUP BY в SQL-запрос. Один из подходов к решению этой проблемы - создать копию поля:

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')

Также отметим, что вы должны быть осторожны с заказом QuerySet. Вам лучше вызвать .order_by() либо без параметров для очистки порядка, либо с вашим полем GROUP BY. Если результирующий запрос будет содержать упорядочение по любому другому полю, группировка будет неправильной. https://docs.djangoproject.com/en/1.11/topics/db/aggregation/#interaction-with-default-ordering-or-order-by

Кроме того, вы можете захотеть удалить эту ложную аннотацию из вашего вывода, поэтому снова вызовите .values(). Итак, финальный код выглядит так:

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')

Ответ 2

Это ожидается от пути group_by works в Django. Все аннотированные поля добавляются в предложение GROUP BY. Тем не менее, я не могу прокомментировать, почему это было написано таким образом.

Вы можете заставить свой запрос работать следующим образом:

Title.objects
  .values('publisher')
  .annotate(total_dbl_price=Sum(2*F('price'))

который создает следующий SQL:

SELECT publisher, SUM((2 * price)) AS total_dbl_price
FROM title
GROUP BY publisher

который просто срабатывает в вашем случае.

Я понимаю, что это может быть не полное решение, которое вы искали, но некоторые даже сложные аннотации также могут быть размещены в этом решении, используя CombinedExpressions (надеюсь!).

Ответ 3

Ваша проблема исходит от values() следовать за annotate(). Порядок важен. Это объясняется в документации по [порядку аннотата и предложениям значений] ( https://docs.djangoproject.com/en/1.10/topics/db/aggregation/#order-of-annotate-and-values-clauses)

.values('pub_id') ограничьте поле queryset с помощью pub_id. Поэтому вы не можете комментировать на income

Метод values ​​() принимает необязательные позиционные аргументы, * поля, которые определяют имена полей, к которым должен быть ограничен SELECT.

Ответ 4

Это решение @alexandr обращается к нему правильно.

fooobar.com/info/335543/...

Что вам нужно, это:

from django.db.models import Sum

Title.objects.values('publisher').annotate(tot_dbl_prices=2*Sum('price'))

В идеале я поменял сценарий здесь, сначала суммируя их, а затем удваивая. Вы пытались удвоить это, а затем подвести итог. Надеюсь, это хорошо.