如何在Django中使用GROUP BY进行查询?

509

我查询一个模型:

Members.objects.all()

它返回:

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

我想知道最佳的Django方法如何向我的数据库发出group_by查询,例如:group_by
Members.objects.all().group_by('designation')

当然是不起作用的。 我知道我们可以在django/db/models/query.py上做一些技巧,但我只是好奇想知道如何在不打补丁的情况下完成它。
13个回答

760

如果您想要做聚合,可以使用ORM的聚合功能

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

这将导致类似于以下查询:

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

输出的格式将会是

[{'designation': 'Salesman', 'dcount': 2}, 
 {'designation': 'Manager', 'dcount': 2}]
如果您不包含order_by(),如果默认排序不符合您的预期,则可能会得到不正确的结果。
如果您想在结果中包含多个字段,请将它们作为参数添加到values中,例如:
    .values('designation', 'first_name', 'last_name')

参考资料:


8
你可以将它串联起来。类似这样:Members.objects.filter(date=some_date).values('designation').annotate(dcount=Count('designation')) - Eli
94
我有一个问题,这个查询只返回了“designation”和“dcount”,如果我想要获取表中的其他值怎么办? - A.J.
24
请注意,如果您的排序字段不是“职称”(designation),则需要重新设置排序才能使其正常工作。请参见https://dev59.com/6XRC5IYBdhLWcg3wVvjL#1341667。 - Gidgidonihah
14
@Gidgidonihah 是的,这个例子应该写成Members.objects.order_by('disignation').values('designation').annotate(dcount=Count('designation')) - bjunix
24
我有一个问题,这个查询只返回了“designation”和“dcount”,如果我想获得表中的其他值怎么办?请帮我翻译。 - Yann叶
显示剩余13条评论

79

有一个容易的解决方法,但不是正确的方式,就是使用原始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来实现类似的结果。


1
请问您能告诉我如何使用order_by来完成吗? - simplyharsh
2
嗨,如果您没有使用聚合功能,您可以通过使用 order_by 来模拟 group_by,并消除您不需要的条目。当然,这只是一种模拟方法,仅在使用少量数据时可用。由于他没有提到聚合功能,我认为这可能是一个解决方案。 - Michael
嘿,这很棒 - 你能解释一下如何使用execute_sql吗?它似乎无法工作。 - rh0dium
19
请注意,这在Django 1.9上已不再适用。 https://dev59.com/IZTfa4cB1Zd3GeqPPViN - grokpot
2
这是一种有点hack的使用ORM的方式。你不应该手动实例化新的queryset并传入旧的queryset。 - Ian Kirkpatrick

59

你也可以使用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 countries_list %}

<ul>
    {% for country in countries_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 标签之前,请通过分组器的键对列表(或查询集)进行排序。


2
太完美了!我找了很久一个简单的方法来做这件事。而且它也适用于查询集,这就是我使用它的方式。 - CarmenA
4
如果您从数据库中读取了大量数据并仅使用聚合值,那么这是完全错误的。 - Sławomir Lenart
1
@SławomirLenart 当然,这可能不像直接的数据库查询那样高效。但对于简单的用例,它可以是一个不错的解决方案。 - inostia
如果结果在模板中显示,那么这将起作用。但是,对于JsonResponse或其他间接响应,这个解决方案将不起作用。 - Willy satrio nugroho
1
@Willysatrionugroho 如果你想在视图中实现这个功能,可以参考这个链接:https://dev59.com/b3RB5IYBdhLWcg3w4bKv - inostia

11
Django不支持自由的group by查询,我非常痛苦地从中学到了这一点。ORM没有设计来支持像你想做的这样的东西,除非使用自定义SQL。你可以使用以下方法:
- 原生SQL (例如:MyModel.objects.raw()) - cr.execute语句(并手动解析结果)。 - .annotate()(对于类似于聚合lines_count=Count('lines')的示例,在子模型中执行group by语句)。
在queryset qs上,您可以调用qs.query.group_by = ['field1', 'field2', ...],但如果您不知道要编辑哪个查询并且没有保证它会起作用且不会破坏QuerySet对象的内部结构,则存在风险。此外,这是一个内部的(未文档化的)API,您不应该直接访问它,否则可能导致代码不再与未来的Django版本兼容。

3
实际上,你不仅在自由group-by方面受到限制,因此建议尝试使用SQLAlchemy代替Django ORM。 - Sławomir Lenart

9
以下模块允许您对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>

与基本的Django查询annotate/aggregate的区别在于使用相关字段的属性,例如book.author.last_name

如果您需要已分组实例的PK,请添加以下注释:

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

注意: ArrayAgg 是一种仅适用于Postgres的特定函数,从Django 1.9开始可用:https://docs.djangoproject.com/en/3.2/ref/contrib/postgres/aggregates/#arrayagg


这个 django-group-byvalues 方法的替代品。我认为它是用于不同的目的。 - LShi
1
@LShi 当然,group_by并不是values的替代品。values是SQL中的select,而group_by是SQL中的group by(正如其名称所示...)。为什么要点踩呢?我们正在生产环境中使用这样的代码来实现复杂的group_by语句。 - Risadinha
它的文档group_by“大多数情况下的行为类似于values方法,但有一个区别...”该文档没有提到SQL GROUP BY,并且它提供的用例也没有暗示它与SQL GROUP BY有任何关系。当有人澄清这一点时,我会撤回我的负评,但是那篇文档确实误导人。 - LShi
阅读了values文档之后,我发现我错过了values本身就像一个GROUP BY的功能。这是我的错。当values不够用时,我认为使用itertools.groupby比使用django-group-by更简单。 - LShi
1
使用简单的values调用或带有annotate但不从数据库中获取所有内容都无法完成上述的group by操作。您提出的itertools.groupby建议适用于小数据集,但对于您可能想要分页的数千个数据集则不适用。当然,在那时,您将不得不考虑一个包含准备好的(已分组)数据的特殊搜索索引。 - Risadinha
显示剩余2条评论

9

你也可以直接使用Python内置的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,子查询,第三方库或模板标签,这是Python风格并且明确易懂的。


9
表现如何? - Ankit Brijwasi

7

文档中提到,您可以使用值来对查询集进行分组。

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

您可以在这里查看一些速查表 这里


为什么你需要使用group_by()函数才能返回正确的结果? - realnot

4

1
一种往返解决方案。如果我需要更多的使用,我会使用它。但是在这里,我只需要每个职称的成员数量就可以了。 - simplyharsh
没问题。我想过提到1.1的聚合特性,但是假设你正在使用发布版本 :) - Van Gale
这完全是关于使用原始查询,这显示了Django的ORM的弱点。 - Sławomir Lenart

2
这有点复杂,但只需进行一次数据库查询即可满足提问者的要求。
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)
)

1

因为某些原因,以上提到的解决方案对我没有起作用。以下是有效的方法:

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

我希望它有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接