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

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

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", но мне просто интересно узнать, как это сделать без исправлений.

14 ответов

Решение

Если вы хотите выполнить агрегирование, вы можете использовать функции агрегации 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}]

Простое решение, но не надлежащим образом, это использовать RAW-SQL:

http://docs.djangoproject.com/en/dev/topics/db/sql/

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

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

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

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

Вы также можете использовать 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

Это также работает на QuerySetЯ верю.

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

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.

Вы также можете использовать встроенные питоны itertools.groupby напрямую:

      from itertools import groupby

designation_key_func = lambda member: member.designation
queryset = Members.objects.all().select_related("designation")

for designation, member_group in groupby(queryset, designation_key_func):
    print(f"{designation} : {list(member_group)}")

Никаких необработанных sql, подзапросов, сторонних библиотек или шаблонных тегов, питонических и явных, на мой взгляд, не требуется.

Существует модуль, который позволяет вам группировать модели 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/aggregate Основные запросы 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/

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

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

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

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

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

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

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

Это немного сложно, но задайте вопрос, чего он/она ожидал от всего одного попадания в БД.

      from django.db.models import Subquery, OuterRef

member_qs = Members.objects.filter(
    pk__in = Members.objects.values('designation').distinct().annotate(
        pk = Subquery(
          Members.objects.filter(
            designation= OuterRef("designation")
        )
        .order_by("pk") # you can set other column, e.g. -pk, create_date...
        .values("pk")[:1]
        ) 
    )
   .values_list("pk", flat=True)
)

Другими словами, если вам нужно просто «удалить дубликаты» на основе какого-либо поля, а в противном случае просто запросить объекты ORM как есть, я придумал следующее обходное решение:

      from django.db.models import OuterRef, Exists

qs = Members.objects.all()
qs = qs.annotate(is_duplicate=Exists(
    Members.objects.filter(
        id__lt=OuterRef('id'),
        designation=OuterRef('designation')))
qs = qs.filter(is_duplicate=False)

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

По какой-то причине вышеупомянутые решения не сработали для меня. Вот что сработало:

      dupes_query = MyModel.objects.all().values('my_field').annotate(
    count=Count('id')
).order_by('-count').filter(count__gt=1)

Я надеюсь, что это помогает.

Если вам нужны объекты модели, а не просто значения или словари, вы можете сделать что-то вроде этого:

      members = Member.objects.filter(foobar=True)
designations = Designation.objects.filter(member__in=members).order_by('pk').distinct()

Заменять member__in с версией в нижнем регистре названия вашей модели, за которой следует __in. Например, если название вашей модели Car, использовать car__in.

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

from django.db.models import Sum
Members.objects.annotate(total=Sum(designation))

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

Другие вопросы по тегам