Как сделать запрос как GROUP BY в Django?

333

Я запрашиваю модель:

Members.objects.all()

И это возвращает:

Eric, Salesman, X-Shop
Freddie, Manager, X2-Shop
Teddy, Salesman, X2-Shop
Sean, Manager, X2-Shop

То, что я хочу, это знать лучший способ Django для отправки group_byзапроса в мою базу данных, например:

Members.objects.all().group_by('designation')

Что не работает, конечно. Я знаю, что мы можем сделать некоторые трюки django/db/models/query.py, но мне просто интересно узнать, как это сделать без исправлений.

simplyharsh
источник

Ответы:

484

Если вы хотите выполнить агрегирование, вы можете использовать функции агрегации ORM :

from django.db.models import Count
Members.objects.values('designation').annotate(dcount=Count('designation'))

Это приводит к запросу, аналогичному

SELECT designation, COUNT(designation) AS dcount
FROM members GROUP BY designation

и результат будет иметь форму

[{'designation': 'Salesman', 'dcount': 2}, 
 {'designation': 'Manager', 'dcount': 2}]
Гудмундур Н
источник
6
@Harry: Вы можете связать это. Что-то вроде:Members.objects.filter(date=some_date).values('designation').annotate(dcount=Count('designation'))
Илай
57
У меня есть вопрос, этот запрос только возвращает обозначение и dcount, что, если я хочу получить другие значения таблицы тоже?
AJ
19
Обратите внимание, что если ваша сортировка является полем, отличным от обозначения, она не будет работать без сброса сортировки. См stackoverflow.com/a/1341667/202137
Gidgidonihah
12
@Gidgidonihah Правда, пример должен читатьсяMembers.objects.order_by('disignation').values('designation').annotate(dcount=Count('designation'))
bjunix
7
У меня есть вопрос, этот запрос только возвращает обозначение и dcount, что, если я хочу получить другие значения таблицы тоже?
Ян 叶
55

Простое, но не правильное решение - использовать сырой SQL :

results = Members.objects.raw('SELECT * FROM myapp_members GROUP BY designation')

Другое решение заключается в использовании group_byсвойства:

query = Members.objects.all().query
query.group_by = ['designation']
results = QuerySet(query=query, model=Members)

Теперь вы можете перебирать переменную результатов, чтобы получить ваши результаты. Обратите внимание, что group_byэто не задокументировано и может быть изменено в будущей версии Django.

И ... почему вы хотите использовать group_by? Если вы не используете агрегацию, вы можете использовать order_byдля достижения одинакового результата.

Майкл
источник
Подскажите пожалуйста, как это сделать используя order_by ??
justharsh
2
Привет, если вы не используете агрегацию, вы можете эмулировать group_by с помощью order_by и удалить ненужные записи. Конечно, это эмуляция, и ее можно использовать только при небольшом объеме данных. Поскольку он не говорил об агрегации, я подумал, что это может быть решением.
Майкл
Эй, это здорово - не могли бы вы объяснить, как использовать execute_sql, который не работает ...
rh0dium
8
Обратите внимание, что это больше не работает на Django 1.9. stackoverflow.com/questions/35558120/…
grokpot
1
Это своего рода хакерский способ использования ORM. Вам не нужно создавать экземпляры новых наборов запросов, передавая старые вручную.
Ян Киркпатрик
33

Вы также можете использовать regroupтег шаблона для группировки по атрибутам. Из документов:

cities = [
    {'name': 'Mumbai', 'population': '19,000,000', 'country': 'India'},
    {'name': 'Calcutta', 'population': '15,000,000', 'country': 'India'},
    {'name': 'New York', 'population': '20,000,000', 'country': 'USA'},
    {'name': 'Chicago', 'population': '7,000,000', 'country': 'USA'},
    {'name': 'Tokyo', 'population': '33,000,000', 'country': 'Japan'},
]

...

{% regroup cities by country as country_list %}

<ul>
    {% for country in country_list %}
        <li>{{ country.grouper }}
            <ul>
            {% for city in country.list %}
                <li>{{ city.name }}: {{ city.population }}</li>
            {% endfor %}
            </ul>
        </li>
    {% endfor %}
</ul>

Выглядит так:

  • Индия
    • Мумбаи: 19 000 000
    • Калькутта: 15 000 000
  • Соединенные Штаты Америки
    • Нью-Йорк: 20 000 000
    • Чикаго: 7 000 000
  • Япония
    • Токио: 33 000 000

Это также работает на QuerySets, я верю.

источник: https://docs.djangoproject.com/en/2.1/ref/templates/builtins/#regroup

edit: обратите внимание, что regroupтег не работает так, как вы ожидаете, если ваш список словарей не отсортирован по ключам. Это работает итеративно. Поэтому сортируйте свой список (или набор запросов) по ключу группировщика, прежде чем передавать его в regroupтег.

inostia
источник
1
Это потрясающе! Я много искал простой способ сделать это. И это также работает с наборами запросов, вот как я это использовал.
CarmenA
1
Это совершенно неправильно, если вы читаете из базы данных большой набор данных, а затем просто используете агрегированные значения.
Славомир Ленарт
@ SławomirLenart, конечно, это может быть не так эффективно, как прямой запрос к БД. Но для простых случаев использования это может быть хорошим решением
inostia
Это будет работать, если результат показан в шаблоне. Но для JsonResponse или другого косвенного ответа. это решение не будет работать.
Вилли Сатрио Нугрохо
1
@Willysatrionugroho, если вы хотите сделать это в виде, например, stackoverflow.com/questions/477820/… может сработать для вас
inostia
7

Вам нужно сделать пользовательский SQL, как показано в этом фрагменте:

Пользовательский SQL через подзапрос

Или в пользовательском менеджере, как показано в онлайн-документации Django:

Добавление дополнительных методов менеджера

Ван Гейл
источник
1
Вид двустороннего решения. Я бы использовал это, если бы у меня было некоторое расширенное использование этого. Но здесь мне просто нужно количество членов на обозначение, вот и все.
justharsh
Нет проблем. Я подумал упомянуть о возможностях агрегации 1.1, но сделал предположение, что вы используете релизную версию :)
Van Gale,
Все дело в использовании необработанных запросов, которые показывают слабость ORM Джанго.
Славомир Ленарт
5

Django не поддерживает свободную группу по запросам . Я узнал это очень плохо. ORM не предназначен для поддержки таких вещей, как то, что вы хотите делать, без использования собственного SQL. Вы ограничены:

  • RAW sql (т.е. MyModel.objects.raw ())
  • cr.execute предложения (и ручной анализ результата).
  • .annotate() (группировка по предложениям выполняется в дочерней модели для .annotate (), в таких примерах, как агрегация lines_count = Count ('lines'))).

Через набор запросов qsвы можете позвонить, qs.query.group_by = ['field1', 'field2', ...]но это рискованно, если вы не знаете, какой запрос вы редактируете, и у вас нет гарантии, что он будет работать и не нарушит внутреннюю часть объекта QuerySet. Кроме того, это внутренний (недокументированный) API, к которому вы не должны обращаться напрямую, не рискуя тем, что код больше не будет совместим с будущими версиями Django.

Луис Масуэлли
источник
на самом деле вы ограничены не только в свободном группировании, поэтому попробуйте SQLAlchemy вместо Django ORM.
Славомир Ленарт
5

Существует модуль, который позволяет вам группировать модели Django и по-прежнему работать с QuerySet в результате: https://github.com/kako-nawao/django-group-by

Например:

from django_group_by import GroupByMixin

class BookQuerySet(QuerySet, GroupByMixin):
    pass

class Book(Model):
    title = TextField(...)
    author = ForeignKey(User, ...)
    shop = ForeignKey(Shop, ...)
    price = DecimalField(...)

class GroupedBookListView(PaginationMixin, ListView):
    template_name = 'book/books.html'
    model = Book
    paginate_by = 100

    def get_queryset(self):
        return Book.objects.group_by('title', 'author').annotate(
            shop_count=Count('shop'), price_avg=Avg('price')).order_by(
            'name', 'author').distinct()

    def get_context_data(self, **kwargs):
        return super().get_context_data(total_count=self.get_queryset().count(), **kwargs)

'Книга / books.html'

<ul>
{% for book in object_list %}
    <li>
        <h2>{{ book.title }}</td>
        <p>{{ book.author.last_name }}, {{ book.author.first_name }}</p>
        <p>{{ book.shop_count }}</p>
        <p>{{ book.price_avg }}</p>
    </li>
{% endfor %}
</ul>

Отличие от annotate/ aggregatebasic запросов Django заключается в использовании атрибутов связанного поля, например book.author.last_name.

Если вам нужны PK экземпляров, которые были сгруппированы вместе, добавьте следующую аннотацию:

.annotate(pks=ArrayAgg('id'))

ПРИМЕЧАНИЕ: ArrayAggэто особая функция Postgres, доступная в Django 1.9 и далее: https://docs.djangoproject.com/en/1.10/ref/contrib/postgres/aggregates/#arrayagg

Risadinha
источник
Этот django-group-by является альтернативой valuesметоду. Я думаю, что для другой цели.
LShi
1
@LShi Это, конечно, не альтернатива ценностям. valuesявляется SQL, selectа group_byявляется SQL group by(как следует из названия ...). Почему отрицательный голос? Мы используем такой код в производстве для реализации сложных group_byоператоров.
Risadinha
В его документе написано group_by«ведет себя в основном как метод значений, но с одним отличием ...» В документе не упоминается SQL, GROUP BYа предоставленный сценарий использования не предполагает, что он имеет какое-либо отношение к SQL GROUP BY. Я откажусь от голосования, когда кто-то прояснил это, но этот документ действительно вводит в заблуждение.
LShi
После прочтения документа дляvalues я обнаружил, что я пропустил, что valuesсамо по себе работает как GROUP BY. Это моя вина. Я думаю, что его проще использовать, itertools.groupbyчем этот django-group-by, когда этого valuesнедостаточно.
LShi
1
Это невозможно сделать group byс помощью простого valuesвызова - с или без annotateи без извлечения всего из базы данных. Ваше предложение itertools.groupbyработает для небольших наборов данных, но не для нескольких тысяч наборов данных, которые вы, вероятно, хотите разместить на странице. Конечно, в этот момент вам придется подумать о специальном поисковом индексе, который в любом случае содержит подготовленные (уже сгруппированные) данные.
Risadinha
0

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

class Travel(models.Model):
    interest = models.ForeignKey(Interest)
    user = models.ForeignKey(User)
    time = models.DateTimeField(auto_now_add=True)

# Find the travel and group by the interest:

>>> Travel.objects.values('interest').annotate(Count('user'))
<QuerySet [{'interest': 5, 'user__count': 2}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited for 2 times, 
# and the interest(id=6) had only been visited for 1 time.

>>> Travel.objects.values('interest').annotate(Count('user', distinct=True)) 
<QuerySet [{'interest': 5, 'user__count': 1}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited by only one person (but this person had 
#  visited the interest for 2 times

Вы можете найти все книги и сгруппировать их по имени, используя этот код:

Book.objects.values('name').annotate(Count('id')).order_by() # ensure you add the order_by()

Вы можете посмотреть некоторые листы здесь .

ramwin
источник
-1

Если я не ошибаюсь, вы можете использовать любой набор запросов .group_by = [' field ']

Рид Джонс
источник
8
Это не так, по крайней мере в Django 1.6: у объекта 'QuerySet' нет атрибута 'group_by'
Факундо Олано
1
Правильное использование может быть queryset.query.group_by = [...], но это нарушит семантику запроса и будет работать не так, как ожидалось.
Луис Масуэлли
-2
from django.db.models import Sum
Members.objects.annotate(total=Sum(designation))

сначала нужно импортировать сумму потом ..

Kiran S канал на YouTube
источник