Як зробити запит як GROUP BY у django?


332

Я запитую модель:

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, але мені просто цікаво знати, як це зробити без виправлення.

Відповіді:


483

Якщо ви хочете зробити агрегацію, ви можете використовувати функції агрегації 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
У мене виникає питання, цей запит повертає лише позначення та знижку, що робити, якщо я хочу отримати й інші значення таблиці?
AJ

19
Зауважте, що якщо ваше сортування - це поле, відмінне від позначення, воно не буде працювати без скидання сортування. Див stackoverflow.com/a/1341667/202137
Gidgidonihah

12
@Gidgidonihah Щоправда, приклад слід читатиMembers.objects.order_by('disignation').values('designation').annotate(dcount=Count('designation'))
bjunix

7
У мене виникає питання, цей запит повертає лише позначення та знижку, що робити, якщо я хочу отримати й інші значення таблиці?
Янн 叶

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 ??
simpleharsh

2
Привіт, якщо ви не використовуєте агрегацію, ви можете наслідувати group_by, використовуючи order_by та усунути записи, які вам не потрібні. Звичайно, це емуляція і корисна лише при використанні не багато даних. Оскільки він не говорив про агрегацію, я подумав, що це може бути рішенням.
Майкл

Гей, це чудово - чи можете ви поясніть, як використовувати Execute_sql, здається, це не працює? ..
rh0dium

8
Зауважте, це більше не працює на Django 1.9. stackoverflow.com/questions/35558120 / ...
grokpot

1
Це своєрідний хакерський спосіб використання ORM. Вам не доведеться створювати нові набори запитів, передаючи старі вручну.
Ян Кіркпатрік

32

Ви також можете використовувати 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

редагувати: зверніть увагу, regroupтег не працює так, як ви його очікували, якщо ваш список словників не буде сортований за ключовими словами. Це працює ітеративно. Тому сортуйте свій список (або набір запитів) за ключем групи, перед тим як передавати його regroupтегу.


1
Це прекрасно! Я багато шукав простий спосіб зробити це. І він працює і на наборах запитів, ось як я його використовував.
КарменА

1
це абсолютно неправильно, якщо ви читаєте з бази даних великий набір даних, а потім просто використовуєте зведені значення.
Славомір Ленарт

@ SławomirLenart впевнений, що це може бути не так ефективно, як прямий запит БД. Але для простих випадків використання це може бути приємне рішення
inostia

Це спрацює, якщо результат, показаний у шаблоні. Але, для JsonResponse чи іншої непрямої відповіді. це рішення не спрацює.
Віллі satrio nugroho

1
@Willysatrionugroho, якщо ви хотіли це зробити з точки зору, наприклад, stackoverflow.com/questions/477820/… може працювати для вас
inostia

7

Вам потрібно зробити спеціальний SQL, як показано в цьому фрагменті:

Спеціальний SQL за допомогою підпиту

Або в спеціальному менеджері, як показано в онлайн-документах Django:

Додавання додаткових методів менеджера


1
Різновид зворотного шляху. Я б використав це, якби у мене було деяке поширене використання цього. Але тут мені просто потрібна кількість членів на позначення, це все.
simpleharsh

Нема проблем. Я подумав згадати функції агрегації 1.1, але зробив припущення, що ви використовуєте версію випуску :)
Van Gale

Вся справа в використанні необроблених запитів, які показують слабкість ORM Django.
Славомір Ленарт

5

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

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

Через набір запитів 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)

'book / 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.

Якщо вам потрібні ПК, з’єднані разом, додайте таку примітку:

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

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


Ця група джанго є альтернативою valuesметоду. Я думаю, що це з різною метою.
LShi

1
@LShi Це, звичайно, не альтернатива значенням. valuesє SQL, selectа group_bySQL group by(як вказує назва ...). Чому потік? Ми використовуємо такий код у виробництві, щоб реалізувати складні group_byзаяви.
Risadinha

Його доктор каже , що group_by«веде себе в основному як методу значення, але з однією відмінністю ...» Док не згадує SQL GROUP BYі варіант використання він забезпечує не передбачає , що це НЕ має нічого спільного з SQL GROUP BY. Я поверну голосування, коли хтось зрозумів це, але цей документ справді вводить в оману.
LShi

Прочитавши документ дляvalues , я виявив, що пропустив, що valuesсам працює як GROUP BY. Це моя вина. Я думаю, що це простіше у використанні, itertools.groupbyніж ця група джанго, коли 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()

Ви можете подивитися деякі cheet лист тут .


-1

Якщо я не помиляюся, ви можете використовувати, будь-який-запит-набір .group_by = [' поле ']


8
Це не так, принаймні, у Django 1.6: "Об'єкт QuerySet" не має атрибута "group_by"
Facundo Olano

1
Правильне використання може бути queryset.query.group_by = [...], але це порушить семантику запиту і не працюватиме, як очікувалося.
Луїс Масуеллі

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.