Эквивалент Django для count и group by

91

У меня есть модель, которая выглядит так:

class Category(models.Model):
    name = models.CharField(max_length=60)

class Item(models.Model):
    name = models.CharField(max_length=60)
    category = models.ForeignKey(Category)

Я хочу выбрать количество (просто количество) элементов для каждой категории, поэтому в SQL это было бы так просто:

select category_id, count(id) from item group by category_id

Есть ли аналог этого "пути Django"? Или единственный вариант - простой SQL? Я знаком с методом count () в Django, но не понимаю, насколько там подходит group by .

Сергей Головченко
источник
Возможный дубликат запроса как GROUP BY в django?
Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
@CiroSantilli 巴拿馬 文件 六四 事件 法轮功 как это дубликат? этот вопрос был задан в 2008 году, а тот, о котором вы говорите, - 2 года спустя.
Сергей Головченко
Текущий консенсус близок к «качеству»: < meta.stackexchange.com/questions/147643/… > Поскольку «качество» не поддается измерению, я просто использую голоса «за». ;-) Скорее всего, все сводится к тому, какой вопрос лучше всего подходит для новичков в Google по названию.
Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

Ответы:

132

Вот, как я только что обнаружил, как это сделать с помощью API агрегации Django 1.1:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))
Майкл
источник
3
как и большинство вещей в Django, ни на что из этого не имеет смысла смотреть, но (в отличие от большинства вещей в Django), когда я действительно попробовал это, это было потрясающе: P
jsh
3
обратите внимание, что вам нужно использовать, order_by()если 'category'это не порядок по умолчанию. (См. Более исчерпывающий ответ Дэниела.)
Рик Вестера,
Причина, по которой это работает, заключается в том, что .annotate()работает несколько иначе после.values() : «Однако, когда предложение values ​​() используется для ограничения столбцов, возвращаемых в наборе результатов, метод оценки аннотаций немного отличается. Вместо возврата аннотированного result для каждого результата в исходном QuerySet, исходные результаты группируются в соответствии с уникальными комбинациями полей, указанными в предложении values ​​() ".
mgalgs
58

( Обновление : полная поддержка агрегации ORM теперь включена в Django 1.1 . В соответствии с приведенным ниже предупреждением об использовании частных API-интерфейсов, описанный здесь метод больше не работает в версиях Django после версии 1.1. Я не стал разбираться, почему; если у вас 1.1 или новее, вам все равно следует использовать настоящий API агрегации .)

Поддержка агрегирования ядра уже была в версии 1.0; он просто недокументирован, не поддерживается и еще не имеет дружественного API. Но вот как вы можете использовать его в любом случае до выхода версии 1.1 (на ваш страх и риск и при полном знании того, что атрибут query.group_by не является частью общедоступного API и может измениться):

query_set = Item.objects.extra(select={'count': 'count(1)'}, 
                               order_by=['-count']).values('count', 'category')
query_set.query.group_by = ['category_id']

Если затем выполнить итерацию query_set, каждое возвращаемое значение будет словарем с ключом «category» и ключом «count».

Здесь нет необходимости заказывать по -count, это просто показано, чтобы продемонстрировать, как это делается (это нужно делать в вызове .extra (), а не где-либо еще в цепочке построения набора запросов). Кроме того, вы также можете сказать count (id) вместо count (1), но последнее может быть более эффективным.

Также обратите внимание, что при настройке .query.group_by значения должны быть фактическими именами столбцов БД ('category_id'), а не именами полей Django ('category'). Это связано с тем, что вы настраиваете внутренние компоненты запроса на уровне, где все определяется терминами БД, а не терминами Django.

Карл Мейер
источник
+1 за старый метод. Даже если в настоящее время не поддерживается, это, мягко говоря, поучительно. Удивительно, правда.
авиаудар
Взгляните на API агрегирования Django по адресу docs.djangoproject.com/en/dev/topics/db/aggregation/ ... с его помощью можно выполнять другие сложные задачи, там вы найдете несколько убедительных примеров.
serfer2
@ serfer2 да, эти документы уже связаны в верхней части этого ответа.
Карл Мейер
56

Поскольку я был немного сбит с толку тем, как работает группировка в Django 1.1, я подумал, что подробно остановлюсь здесь на том, как именно вы собираетесь ее использовать. Во-первых, повторим то, что сказал Майкл:

Вот, как я только что обнаружил, как это сделать с помощью API агрегации Django 1.1:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))

Также обратите внимание, что вам необходимо from django.db.models import Count!

Это выберет только категории, а затем добавит аннотацию с именем category__count. В зависимости от порядка по умолчанию это может быть все, что вам нужно, но если в порядке по умолчанию используется другое поле, categoryто работать не будет . Причина в том, что поля, необходимые для упорядочивания, также выбраны и делают каждую строку уникальной, поэтому вы не сможете сгруппировать данные так, как вы этого хотите. Один из быстрых способов исправить это - сбросить порядок:

Item.objects.values('category').annotate(Count('category')).order_by()

Это должно дать именно тот результат, который вам нужен. Чтобы задать имя аннотации, вы можете использовать:

...annotate(mycount = Count('category'))...

Тогда у вас будет аннотация, вызываемая mycountв результатах.

Все остальное в группировке было для меня очень простым. Обязательно ознакомьтесь с API агрегирования Django для получения более подробной информации.

Даниэль
источник
1
для выполнения того же набора действий с полем внешнего ключа Item.objects.values ​​('category__category'). annotate (Count ('category__category')). order_by ()
Mutant
Как определить, какое поле для упорядочивания по умолчанию?
Богатырь
2

Как это? (Кроме медленного.)

counts= [ (c, Item.filter( category=c.id ).count()) for c in Category.objects.all() ]

Его преимущество в том, что он короткий, даже если он получает много строк.


Редактировать.

Одна версия запроса. Кстати, это часто быстрее, чем SELECT COUNT (*) в базе данных. Попробуйте это увидеть.

counts = defaultdict(int)
for i in Item.objects.all():
    counts[i.category] += 1
С.Лотт
источник
Это красиво и коротко, однако я бы хотел избежать отдельного вызова базы данных для каждой категории.
Сергей Головченко
Это действительно хороший подход для простых случаев. Он падает, когда у вас есть большой набор данных, и вы хотите упорядочить + limit (т.е. разбить на страницы) в соответствии с количеством, не вытягивая тонны ненужных данных.
Карл Мейер,
@Carl Meyer: Верно - это может показаться собачкой для большого набора данных; однако, чтобы убедиться в этом, вам нужно провести тест. Кроме того, он также не полагается на неподдерживаемый материал; он работает до тех пор, пока не будут поддерживаться неподдерживаемые функции.
S.Lott