聚合

    整篇指南我们将引用以下模型。这些模型用来记录多个网上书店的库存。

    下面是根据以上模型执行常见的聚合查询:

    1. # Total number of books.
    2. >>> Book.objects.count()
    3. 2452
    4. # Total number of books with publisher=BaloneyPress
    5. >>> Book.objects.filter(publisher__name='BaloneyPress').count()
    6. 73
    7. # Average price across all books.
    8. >>> from django.db.models import Avg
    9. >>> Book.objects.all().aggregate(Avg('price'))
    10. {'price__avg': 34.35}
    11. # Max price across all books.
    12. >>> from django.db.models import Max
    13. >>> Book.objects.all().aggregate(Max('price'))
    14. {'price__max': Decimal('81.20')}
    15. # Difference between the highest priced book and the average price of all books.
    16. >>> from django.db.models import FloatField
    17. >>> Book.objects.aggregate(
    18. ... price_diff=Max('price', output_field=FloatField()) - Avg('price'))
    19. {'price_diff': 46.85}
    20. # All the following queries involve traversing the Book<->Publisher
    21. # foreign key relationship backwards.
    22. # Each publisher, each with a count of books as a "num_books" attribute.
    23. >>> from django.db.models import Count
    24. >>> pubs = Publisher.objects.annotate(num_books=Count('book'))
    25. >>> pubs
    26. <QuerySet [<Publisher: BaloneyPress>, <Publisher: SalamiPress>, ...]>
    27. >>> pubs[0].num_books
    28. 73
    29. # Each publisher, with a separate count of books with a rating above and below 5
    30. >>> from django.db.models import Q
    31. >>> below_5 = Count('book', filter=Q(book__rating__lte=5))
    32. >>> pubs = Publisher.objects.annotate(below_5=below_5).annotate(above_5=above_5)
    33. >>> pubs[0].above_5
    34. 23
    35. >>> pubs[0].below_5
    36. 12
    37. # The top 5 publishers, in order by number of books.
    38. >>> pubs = Publisher.objects.annotate(num_books=Count('book')).order_by('-num_books')[:5]
    39. >>> pubs[0].num_books
    40. 1323

    Django 提供了两种生成聚合的方法。第一种方法是从整个 QuerySet 生成汇总值。比如你想要计算所有在售书的平均价格。Django 的查询语法提供了一种用来描述所有图书集合的方法:

    1. >>> Book.objects.all()

    可以通过在 QuerySet 后添加 aggregate() 子句来计算 QuerySet 对象的汇总值。

    1. >>> from django.db.models import Avg
    2. >>> Book.objects.all().aggregate(Avg('price'))
    3. {'price__avg': 34.35}

    本例中的 all() 是多余的,所以可以简化成这样的:

    1. >>> Book.objects.aggregate(Avg('price'))
    2. {'price__avg': 34.35}

    传递给 aggregate() 的参数描述了我们想要计算的聚合值。在这个例子里,要计算的就是 Book 模型上的 price 字段的平均值。可用的聚合函数列表可以在 中找到。

    aggregate()QuerySet 的一个终端子句,使用后将返回“名称-值”的字典,其中“名称”就是聚合值的标志,“值”就是计算出的聚合结果。“名称”是根据字段名和聚合函数而自动生成的。如果你想指定一个聚合值的名称,你可以在指定聚合子句的时候提供指定的名称:

    1. >>> Book.objects.aggregate(average_price=Avg('price'))
    2. {'average_price': 34.35}

    如果你想生成更多的聚合内容,你需要在 aggregate() 子句中加入其它参数即可。所以,如果我们也想知道所有书中最高和最低的价格,我们可以写这样的查询:

    1. >>> from django.db.models import Avg, Max, Min
    2. >>> Book.objects.aggregate(Avg('price'), Max('price'), Min('price'))
    3. {'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')}

    生成值的汇总的另一个办法是为 QuerySet 的每一个对象生成独立汇总。比如,如果你想检索书籍列表,你可能想知道每一本书有多少作者。每一本书与作者有多对多的关系;我们想在 QuerySet 中为每一本书总结这个关系。

    使用 子句可以生成每一个对象的汇总。当指定 annotate() 子句,QuerySet 中的每一个对象将对指定值进行汇总。

    这些汇总语法规则与 aggregate() 子句的规则相同。annotate() 的每一个参数描述了一个要计算的聚合。比如,注解(annotate)所有书的所有作者:

    1. # Build an annotated queryset
    2. >>> q = Book.objects.annotate(Count('authors'))
    3. # Interrogate the first object in the queryset
    4. >>> q[0]
    5. <Book: The Definitive Guide to Django>
    6. >>> q[0].authors__count
    7. 2
    8. # Interrogate the second object in the queryset
    9. >>> q[1]
    10. <Book: Practical Django Projects>
    11. >>> q[1].authors__count
    12. 1

    aggregate() 一样,注解的名称是根据聚合函数和被聚合的字段名自动生成的。当你在指定注解的时候,你可以通过提供一个别名重写这个默认名:

    1. >>> q = Book.objects.annotate(num_authors=Count('authors'))
    2. >>> q[0].num_authors
    3. 2
    4. >>> q[1].num_authors
    5. 1

    aggregate() 不同的是,annotate() 不是终端子句。annotate() 子句的输出就是 QuerySet;这个 QuerySet 被其他 QuerySet 操作进行修改,包括 filter()`, order_by() ,甚至可以对 annotate() 进行额外调用。

    使用 annotate() 组合多个聚合将产生错误的结果( ),因为它使用连接(joins)而不是子查询:

    1. >>> book = Book.objects.first()
    2. >>> book.authors.count()
    3. 2
    4. >>> book.store_set.count()
    5. 3
    6. >>> q = Book.objects.annotate(Count('authors'), Count('store'))
    7. >>> q[0].authors__count
    8. 6
    9. >>> q[0].store__count
    10. 6

    对大部分聚合来说,没办法避免这个问题,但是,Count 聚合可以使用 distinct 参数来避免:

    如有疑问,请检查 SQL 查询!

    为了搞清楚你的查询发生了什么问题,你得在 QuerySet 中检查一下``query`` 属性。

    到目前为止,我们已经处理了被查询模型字段的聚合。然而,有时候想聚合的值属于你正在查询模型的关联模型。

    在聚合函数里面指定聚合的字段时,Django 允许你在过滤相关字段的时候使用相同的双下划线表示法。Django 将处理任何需要检索和聚合的关联值的表连接(table joins)。

    比如,要寻找每个书店提供的书籍价格区间,你可以使用这个注解(annotation):

    1. >>> from django.db.models import Max, Min
    2. >>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price'))

    这告诉 Django 去检索 Store 模型,连接(通过多对多关系) Book 模型,并且聚合书籍模型的价格字段来获取最大最小值。

    相同规则应用于 aggregate() 从句。如果你想知道任何店铺正在销售的任何书籍的最低最高价,你可以使用这个聚合:

      Join 链可以根据你的需求尽可能深。比如,要提取所出售的书籍中最年轻的作者年龄,你可以写这样的查询:

      1. >>> Store.objects.aggregate(youngest_age=Min('books__authors__age'))

      反向关系

      类似于 ,你正在查询的在模型和模型字段上的聚合和注解(annotations)可以包含反向关系。关系模型的小写名和双下划线也可以用在这里。

      1. >>> from django.db.models import Avg, Count, Min, Sum
      2. >>> Publisher.objects.annotate(Count('book'))

      (查询结果里的每一个 Publisher 会有多余的属性—— book__count 。)

      我们也可以找出最古老的一本:

      1. >>> Publisher.objects.aggregate(oldest_pubdate=Min('book__pubdate'))

      (结果字典中会有一个叫 'oldest_pubdate' 的键。如果没有指定这样的别名,它将会是一个很长的名字 'book__pubdate__min' 。)

      它不仅仅用于外键,它也适用于多对多关系。比如,我们能查询每一个作者,注解作者(共同)创作的书籍总页数(注意我们如何使用 'book' 来指定 Author -> Book 反向多对多跳转):

      1. >>> Author.objects.annotate(total_pages=Sum('book__pages'))

      (结果集里的每一个 Author 会有一个额外的属性——total_pages)如果没有指定这样的别名,它将会是一个很长的名字 book__pages__sum

      或查询书籍的平均评分:

      1. >>> Author.objects.aggregate(average_rating=Avg('book__rating'))

      (结果字典会有一个叫 'average_rating' 的键。如果没有指定这样的别名,它将会是一个很长的名字 'book__rating__avg'。)

      聚合也可以参与过滤。任何应用于普通模型字段的 filter() (或 exclude())会具有约束被认为是聚合的对象的效果。

      当使用 annotate() 从句,过滤器具有约束计算注解的对象的效果。比如,你可以使用查询生成一个所有书籍的注解列表,这个列表的标题以 “Django” 开头。

      1. >>> from django.db.models import Avg, Count
      2. >>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors'))

      当使用 aggregate() 子句,过滤器将具有约束计算聚合的对象的效果。比如,你可以使用查询生成所有标题以 “Django” 开头的平均价格。

      1. >>> Book.objects.filter(name__startswith="Django").aggregate(Avg('price'))

      过滤注解

      注解过的值也可以使用过滤器。注解的别名可以和任何其他模型字段一样使用 filter()exclude() 子句。

      比如,要生成多名作者的书籍列表,可以发出这种查询:

      这个查询生成一个注解结果集,然后生成一个基于注解的过滤器。

      如果你需要两个带有两个独立的过滤器的注解,你可以在任何聚合中使用 filter 语句。比如,要生成一个带有高评价书籍的作者列表:

      1. >>> highly_rated = Count('book', filter=Q(book__rating__gte=7))
      2. >>> Author.objects.annotate(num_books=Count('book'), highly_rated_books=highly_rated)

      结果集中的每一个``Author`` 会有 num_bookshighly_rated_books 属性。

      filterQuerySet.filter() 中做选择

      避免在单个注解和聚合中使用 filter 语句。使用 QuerySet.filter() 来排除列会很高效。聚合 filter 语句只在使用具有不同条件的相同关系的两个或以上的聚合时有用。

      annotate()filter() 子句的顺序

      当开发一个涉及 annotate()filter() 子句的复杂查询时,要特别注意应用于 的子句的顺序。

      当一个 annotate() 子句应用于查询,会根据查询状态来计算注解,直到请求的注解为止。这实际上意味着 filter()annotate() 不是可交换的操作。

      比如:

      • 出版者A有两本评分4和5的书。
      • 出版者B有两本评分1和4的书。
      • 出版者C有一本评分1的书。

      下面就是 Count 聚合的例子:

      1. >>> a, b = Publisher.objects.annotate(num_books=Count('book', distinct=True)).filter(book__rating__gt=3.0)
      2. >>> a, a.num_books
      3. (<Publisher: A>, 2)
      4. >>> b, b.num_books
      5. (<Publisher: B>, 2)
      6. >>> a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(num_books=Count('book'))
      7. >>> a, a.num_books
      8. (<Publisher: A>, 2)
      9. >>> b, b.num_books
      10. (<Publisher: B>, 1)

      两个查询返回出版者列表,这些出版者至少有一本评分3的书,因此排除了C。

      在第一个查询里,注解优先于过滤器,因此过滤器没有影响注解。distinct=True 用来避免 a query bug

      第二个查询每个发布者评分3以上的书籍数量。过滤器优先于注解,因此过滤器约束计算注解时考虑的对象。

      1. >>> a, b = Publisher.objects.annotate(avg_rating=Avg('book__rating')).filter(book__rating__gt=3.0)
      2. >>> a, a.avg_rating
      3. (<Publisher: A>, 4.5) # (5+4)/2
      4. >>> b, b.avg_rating
      5. (<Publisher: B>, 2.5) # (1+4)/2
      6. >>> a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(avg_rating=Avg('book__rating'))
      7. >>> a, a.avg_rating
      8. (<Publisher: A>, 4.5) # (5+4)/2
      9. >>> b, b.avg_rating
      10. (<Publisher: B>, 4.0) # 4/1 (book with rating 1 excluded)

      第一个查询请求至少有一本评分3以上的书籍的出版者的书籍平均分。第二个查询只请求评分3以上的作者书籍的平均评分。

      很难凭直觉了解ORM如何将复杂的查询集转化为SQL查询,因此当有疑问时,请使用 str(queryset.query)` 检查SQL,并写大量的测试。

      order_by()

      注解可以当做基本排序来使用。当你定义了一个 order_by() 子句,你提供的聚合可以引用任何定义为查询中 annotate() 子句的一部分的别名。

      比如,通过书籍的作者数量来对书籍的 QuerySet 排序,你可以使用下面的查询:

      1. >>> Book.objects.annotate(num_authors=Count('authors')).order_by('num_authors')

      通常,注解值会添加到每个对象上,即一个被注解的 QuerySet 将会为初始 QuerySet 的每个对象返回一个结果集。然而,当使用 values() 子句来对结果集进行约束时,生成注解值的方法会稍有不同。不是在原始 QuerySet 中对每个对象添加注解并返回,而是根据定义在 values() 子句中的字段组合先对结果进行分组,再对每个单独的分组进行注解,这个注解值是根据分组中所有的对象计算得到的。

      下面是一个关于作者的查询例子,查询每个作者所著书的平均评分:

      1. >>> Author.objects.annotate(average_rating=Avg('book__rating'))

      这段代码返回的是数据库中的所有作者及其所著书的平均评分。

      但是如果你使用 values() 子句,结果会稍有不同:

      1. >>> Author.objects.values('name').annotate(average_rating=Avg('book__rating'))

      在这个例子中,作者会按名字分组,所以你只能得到不重名的作者分组的注解值。这意味着如果你有两个作者同名,那么他们原本各自的查询结果将被合并到同一个结果中;两个作者的所有评分都将被计算为一个平均分。

      annotate()values() 的顺序

      和使用 filter() 一样,作用于某个查询的 annotate()values() 子句的顺序非常重要。如果 values() 子句在 annotate() 之前,就会根据 values() 子句产生的分组来计算注解。

      然而如果 annotate() 子句在 values() 之前,就会根据整个查询集生成注解。这种情况下,values() 子句只能限制输出的字段。

      举个例子,如果我们颠倒上个例子中 values()annotate() 的顺序:

      1. >>> Author.objects.annotate(average_rating=Avg('book__rating')).values('name', 'average_rating')

      这段代码将为每个作者添加一个唯一注解,但只有作者姓名和 average_rating 注解会返回在输出结果中。

      你应该也会注意 average_rating 已经明确包含在返回的值列表中。这是必需的,因为 values()annotate() 子句的顺序。

      如果 values() 子句在 annotate() 子句之前,任何注解将自动添加在结果集中。然而,如果 values() 子句应用在 annotate() 子句之后,则需要显式包含聚合列。

      与默认排序或 order_by() 交互

      在选择输出数据的时候,将使用在查询集的 order_by() 中提及的字段(或模型上的默认排序中使用的字段),即使它们不是其他在 values() 中指定的调用。这些额外的字段用于将“相似”的结果分组在一起,它们可以使其他相同的结果行看起来是分开的。尤其是在数数的时候。

      举个例子,假设你有这样的模型:

      1. from django.db import models
      2. class Item(models.Model):
      3. name = models.CharField(max_length=10)
      4. data = models.IntegerField()
      5. class Meta:
      6. ordering = ["name"]

      这里的重点是在 name 字段上的默认排序。如果你想计算每一个不同的 data 值出现次数,你可以这样做:

      1. # Warning: not quite correct!
      2. Item.objects.values("data").annotate(Count("id"))

      它将通过共同的 data 值对 Item 对象进行分组,然后在每一组里计算 id 值的数量。只可惜它不能正常工作。通过 name 进行的默认排序也在分组中起作用,因此这个查询将按不同的 (data, name) 分组,这不是你想要的。相反,你可以构建这样的查询集:

      清除任何查询中的排序。你也可以通过 data 排序,没有任何有害影响,因为它已经在查询中发挥了作用。

      这个行为与 的查询文档指出的行为相同,一般规则是一样的:通常情况下,你不希望额外的列在结果中发挥作用,因此要清除排序,或者至少确保它只限于您在 values() 调用中选择的那些字段。

      注解

      你可以会问为什么 Django 没有移除无关的列。主要原因就是与 distinct() 和其他地方的一致性:Django从不删除你指定的排序约束(我们不能改变其他方法的行为,因为这会违反我们的应用编程接口的稳定性政策)。

      聚合注解

      你也可以在注解结果上生成聚合。当你定义 aggregate() 子句时,你提供的聚合可以引用任何定义在查询中 annotate() 子句的别名。

      1. >>> from django.db.models import Avg, Count
      2. >>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors'))