Как сделать SELECT COUNT (*) GROUP BY и ORDER BY в Django?

99

Я использую модель транзакции, чтобы отслеживать все события, происходящие в системе

class Transaction(models.Model):
    actor = models.ForeignKey(User, related_name="actor")
    acted = models.ForeignKey(User, related_name="acted", null=True, blank=True)
    action_id = models.IntegerField() 
    ......

как мне получить 5 лучших актеров в моей системе?

В sql это будет в основном

SELECT actor, COUNT(*) as total 
FROM Transaction 
GROUP BY actor 
ORDER BY total DESC
тоторо
источник

Ответы:

181

Согласно документации, вы должны использовать:

from django.db.models import Count
Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total')

values ​​(): указывает, какие столбцы будут использоваться для "группировки по"

Документы Django:

"Когда предложение values ​​() используется для ограничения столбцов, возвращаемых в наборе результатов, метод оценки аннотаций немного отличается. Вместо того, чтобы возвращать аннотированный результат для каждого результата в исходном QuerySet, исходные результаты группируются в соответствии с к уникальным комбинациям полей, указанным в предложении values ​​() "

annotate (): указывает операцию над сгруппированными значениями

Документы Django:

Второй способ создания сводных значений - это создание независимой сводки для каждого объекта в QuerySet. Например, если вы получаете список книг, вы можете узнать, сколько авторов внесли свой вклад в каждую книгу. Каждая Книга имеет отношение «многие ко многим» с Автором; мы хотим резюмировать эти отношения для каждой книги в QuerySet.

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

Порядок по пункту не требует пояснений.

Подводя итог: вы группируете, создавая набор запросов авторов, добавляете аннотацию (это добавит дополнительное поле к возвращаемым значениям) и, наконец, вы упорядочиваете их по этому значению

Обратитесь к https://docs.djangoproject.com/en/dev/topics/db/aggregation/ для получения дополнительной информации.

На заметку: при использовании Count значение, переданное в Count, не влияет на агрегацию, а только имя, присвоенное окончательному значению. Агрегатор группирует по уникальным комбинациям значений (как упомянуто выше), а не по значению, переданному в Count. Следующие запросы такие же:

Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total')
Transaction.objects.all().values('actor').annotate(total=Count('id')).order_by('total')
Альваро
источник
Для меня это сработало Transaction.objects.all().values('actor').annotate(total=Count('actor')).order_by('total'), не забудьте импортировать Count из django.db.models. Спасибо
Иванчо
3
Полезно отметить: при использовании Count(и, возможно, других агрегаторов) значение, переданное в Count, не влияет на агрегирование, а только имя, присвоенное окончательному значению. Агрегатор группирует по уникальным комбинациям values(как упоминалось выше), а не по переданному значению Count.
kronosapiens
Вы даже можете использовать это для наборов запросов результатов поиска postgres, чтобы получить фасетирование!
yekta 04
2
@kronosapiens Это действительно влияет на это, по крайней мере, в настоящее время (я использую Django 2.1.4). В примере totalуказано имя, а в sql используется счетчик, COUNT('actor')который в этом случае не имеет значения, но если, например values('x', 'y').annotate(count=Count('x')), вы получите COUNT(x), нет COUNT(*)или COUNT(x, y)просто попробовали это в./manage.py shell
timdiels
35

Точно так же, как @Alvaro ответил на прямой эквивалент Django для GROUP BYоператора:

SELECT actor, COUNT(*) AS total 
FROM Transaction 
GROUP BY actor

это за счет использования values()и annotate()методов следующим образом :

Transaction.objects.values('actor').annotate(total=Count('actor')).order_by()

Однако следует отметить еще одну вещь:

Если в модели определен порядок по умолчанию class Meta, это .order_by()условие является обязательным для получения правильных результатов. Вы просто не можете пропустить его, даже если заказ не предназначен.

Кроме того, для получения высококачественного кода рекомендуется всегда ставить .order_by()предложение после annotate(), даже если его нет class Meta: ordering. Такой подход сделает заявление перспективным: оно будет работать так, как задумано, независимо от любых будущих изменений в class Meta: ordering.


Позвольте привести вам пример. Если в модели были:

class Transaction(models.Model):
    actor = models.ForeignKey(User, related_name="actor")
    acted = models.ForeignKey(User, related_name="acted", null=True, blank=True)
    action_id = models.IntegerField()

    class Meta:
        ordering = ['id']

Тогда такой подход НЕ БУДЕТ работать:

Transaction.objects.values('actor').annotate(total=Count('actor'))

Это потому, что Django выполняет дополнительные операции GROUP BYдля каждого поля вclass Meta: ordering

Если вы напечатаете запрос:

>>> print Transaction.objects.values('actor').annotate(total=Count('actor')).query
  SELECT "Transaction"."actor_id", COUNT("Transaction"."actor_id") AS "total"
  FROM "Transaction"
  GROUP BY "Transaction"."actor_id", "Transaction"."id"

Будет ясно, что агрегирование НЕ будет работать так, как задумано, и поэтому .order_by()необходимо использовать предложение, чтобы очистить это поведение и получить правильные результаты агрегирования.

См .: Взаимодействие с порядком по умолчанию или order_by () в официальной документации Django.

Krzysiek
источник
3
.order_by()спас меня от orderingметы.
Бабкен Варданян