Агрегация аннотации в GROUP BY в Джанго
ОБНОВИТЬ
Благодаря опубликованному ответу я нашел гораздо более простой способ сформулировать проблему. Оригинальный вопрос можно увидеть в истории изменений.
Эта проблема
Я пытаюсь перевести запрос 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's 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
Джанго запрос
Запрос будет выглядеть так:
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 () предшествует annotate(), аннотация будет вычислена с использованием группировки, описанной предложением values ().
что является основой того, как Django реализует SQLGROUP 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) SELECT
список) и (2) группировка + агрегирование по группам (SQL GROUP BY
) - и в этом случае эти два способа кажутся противоречивыми.
Мой вопрос: есть ли способ решить эту проблему (без таких вещей, как возврат к raw SQL)?
Обратите внимание: конкретный пример может быть решен путем перемещения всех annotate
заявления после values
, который был отмечен несколькими ответами. Однако меня больше интересуют решения (или обсуждение), которые бы annotate
заявление перед values()
по трем причинам: 1. Существуют также более сложные примеры, где предложенный обходной путь не будет работать. 2. Я могу представить себе ситуации, когда аннотированный набор запросов был передан другой функции, которая фактически выполняет GROUP BY, так что единственное, что мы знаем, это набор имен аннотированных полей и их типы. 3. Ситуация кажется довольно простой, и меня удивило бы, если бы это столкновение двух разных видов использования values()
не был замечен и обсужден ранее.
4 ответа
Возможно, уже слишком поздно, но я нашел решение (протестировано с Django 1.11.1).
Проблема в том, позвоните .values('publisher')
, который требуется для обеспечения группировки, удаляет все аннотации, которые не включены в .values()
поля
И мы не можем включить dbl_price
в поля param, потому что это добавит еще один GROUP BY
заявление.
Решение сделать все агрегации, которые сначала требуют аннотированных полей, а затем вызвать .values()
и включить эту агрегацию в поля param(это не добавит GROUP BY
потому что они являются скоплениями). Тогда мы должны позвонить .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/
Кроме того, вы можете захотеть удалить эту ложную аннотацию из вашего вывода, поэтому снова вызовите.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')
Это ожидается от того, как group_by работает в 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(я надеюсь!).
Ваша проблема исходит от values()
следовать annotate()
, Порядок важен. Это объясняется в документации о [порядке аннотирования и значениях] ( https://docs.djangoproject.com/en/1.10/topics/db/aggregation/)
.values('pub_id')
ограничить поле набора запросов pub_id
, Так что вы не можете комментировать income
Метод values () принимает необязательные позиционные аргументы, * поля, которые определяют имена полей, которыми должен быть ограничен SELECT.
Что вам нужно, это:
from django.db.models import Sum
Title.objects.values('publisher').annotate(tot_dbl_prices=2*Sum('price'))
В идеале я поменял сценарий здесь, сначала суммируя их, а затем удваивая. Вы пытались удвоить это, а затем подвести итог. Надеюсь, это хорошо.