Как сделать запрос как 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))
сначала нужно импортировать сумму потом..