【问题标题】:Django 1.11 Annotating a Subquery AggregateDjango 1.11 注释子查询聚合
【发布时间】:2017-07-21 12:04:56
【问题描述】:

这是一个前沿功能,我目前非常关注它并很快就被淘汰了。我想将子查询聚合注释到现有查询集上。在 1.11 之前执行此操作意味着自定义 SQL 或锤击数据库。 Here's the documentation for this,以及它的例子:

from django.db.models import OuterRef, Subquery, Sum
comments = Comment.objects.filter(post=OuterRef('pk')).values('post')
total_comments = comments.annotate(total=Sum('length')).values('total')
Post.objects.filter(length__gt=Subquery(total_comments))

他们在聚合上注释,这对我来说似乎很奇怪,但无论如何。

我正在为此苦苦挣扎,所以我将其归结为我拥有数据的最简单的真实示例。我有Carparks,其中包含许多Spaces。使用Book→Author,如果这让你更快乐,但是——现在——我只想用Subquery*注释相关模型的计数。

spaces = Space.objects.filter(carpark=OuterRef('pk')).values('carpark')
count_spaces = spaces.annotate(c=Count('*')).values('c')
Carpark.objects.annotate(space_count=Subquery(count_spaces))

这给了我一个可爱的ProgrammingError: more than one row returned by a subquery used as an expression,在我的脑海中,这个错误非常有意义。子查询返回带有注释总数的空格列表。

这个例子表明某种魔法会发生,我最终会得到一个我可以使用的数字。但这不是在这里发生的吗?如何对聚合子查询数据进行注释?

嗯,正在向我的查询的 SQL 添加一些内容...

我建立了一个新的停车场/空间模型,它工作正常。所以下一步是找出是什么毒害了我的 SQL。在 Laurent 的建议下,我查看了 SQL 并试图使其更像他们在答案中发布的版本。这就是我发现真正问题的地方:

SELECT "bookings_carpark".*, (SELECT COUNT(U0."id") AS "c"
FROM "bookings_space" U0
WHERE U0."carpark_id" = ("bookings_carpark"."id")
GROUP BY U0."carpark_id", U0."space"
)
AS "space_count" FROM "bookings_carpark";

我已经突出显示了它,但它是那个子查询的GROUP BY ... U0."space"。出于某种原因,它正在重新调整两者。调查仍在继续。

编辑 2:好的,只需查看子查询 SQL,我可以通过☹看到第二组

In [12]: print(Space.objects_standard.filter().values('carpark').annotate(c=Count('*')).values('c').query)
SELECT COUNT(*) AS "c" FROM "bookings_space" GROUP BY "bookings_space"."carpark_id", "bookings_space"."space" ORDER BY "bookings_space"."carpark_id" ASC, "bookings_space"."space" ASC

编辑 3:好的!这两种模型都有排序顺序。这些正在执行到子查询。正是这些命令使我的查询膨胀并破坏了它。

我猜这可能是 Django 中的一个错误,但没有删除这两个模型上的 Meta-order_by,有什么方法可以在查询时取消排序查询?


*我知道我可以为这个例子添加一个 Count 注释。我使用它的真正目的是一个更复杂的过滤器计数,但我什至无法让它工作。

【问题讨论】:

  • 嗯,目前的答案表明这是可能的。我的模型中可能有一些我没有考虑过的东西会影响我的这个。我会在星期一完成它,如果需要的话,我会从零开始构建它,看看它在哪里崩溃。
  • 我认为你不应该在.annotate 之前使用values,因为这会创建grouping。你可以试试这个queryset:Carpark.objects.annotate(space_count=Subquery(Space.objects.filter(carpark=OuterRef('pk')).annotate(space_cnt=Count('pk')).values('space_cnt')))
  • 感谢 Todor,但仍会引发“用作表达式的子查询返回的不止一行”。我将重建模型以确保我的项目中没有任何干扰。
  • @Oli 在您的第二次编辑中,您似乎正在使用自定义管理器 (objects_standard)。这是您的额外分组的来源吗?另外,bookings_space 上的 space 属性是什么?
  • 如果您不希望对查询应用任何排序,甚至不希望使用默认排序,请在不带参数的情况下调用 order_by()。

标签: django django-aggregation django-annotate django-subquery


【解决方案1】:

沙赞!根据我的编辑,我的子查询正在输出一个额外的列。这是为了方便订购(在 COUNT 中不需要)。

我只需要从模型中删除规定的元顺序。您可以通过在子查询中添加一个空的.order_by() 来做到这一点。在我的代码术语中,这意味着:

from django.db.models import Count, OuterRef, Subquery

spaces = Space.objects.filter(carpark=OuterRef('pk')).order_by().values('carpark')
count_spaces = spaces.annotate(c=Count('*')).values('c')
Carpark.objects.annotate(space_count=Subquery(count_spaces))

这行得通。太棒了。好烦。

【讨论】:

  • 我很困惑为什么需要任何订购。您是否会使用 mysql 作为后端? Django 源代码在db/backends/base/features.py 中有requires_explicit_null_ordering_when_grouping = False,只有mysql 设置为True。除了在单元测试 (github.com/django/django/blob/…) 之外,我无法弄清楚它在哪里使用,但这可以解释为什么你会得到这个额外的 ORDER BY。
  • 不,所应用的排序是“自然”模型指定的排序。但它破坏了这些子查询。我想我发现了一个错误。复制非常简单。
  • 事实证明,Subquery 的 Exists 子类删除了排序(实际上是出于性能原因,因为没有理由对子查询进行排序)。这是(非常轻松地)记录的,但也许Subquery 的文档也可以改进。
  • 感谢您的这篇文章。关于 Django 1.11 中引入的子查询,仍然没有足够的适当文档。这是迄今为止最好的问题解决者。
  • 这应该是公认的答案!它可以生成更短、更高效的 SQL。
【解决方案2】:

还可以创建Subquery 的子类,以更改其输出的 SQL。例如,您可以使用:

class SQCount(Subquery):
    template = "(SELECT count(*) FROM (%(subquery)s) _count)"
    output_field = models.IntegerField()

然后您可以像使用原始 Subquery 类一样使用它:

spaces = Space.objects.filter(carpark=OuterRef('pk')).values('pk')
Carpark.objects.annotate(space_count=SQCount(spaces))

你可以在一系列聚合函数中使用这个技巧(至少在 postgres 中):我经常用它来构建一个值数组,或者对它们求和。

【讨论】:

  • 这是一个有趣的想法(并且相当整洁,即使名称冲突)但是如果我遇到的底层错误(模型定义的排序添加是额外的分组依据)仍然存在于子查询 SQL 组合中,这也会击中它。
  • 我不确定它会点击额外的分组名称,因为子查询在另一个子查询中使用,它返回单个值。
  • 感谢您的技巧!值得一提的是,您可以将此技巧与@karolyi 答案结合使用,以删除您可以在查询集中拥有的任何订单,只需要在 SQCount 类上编辑 resolve_expression。
  • 这会生成大量不需要的 SQL。子查询变为:(SELECT count(SELECT id, field1, field2, field3, ... FROM space WHERE ..))。以下来自@Oli 的解决方案表现得更好,变为(SELECT COUNT(*) FROM space WHERE ..)
  • 你确定吗? SQCount 子查询使用一个显式模板,该模板只执行count(*):它根本不包括其他列。
【解决方案3】:

我刚刚遇到了一个非常相似的案例,我必须为未取消预订状态的活动预订座位。在试图解决问题几个小时后,我认为这是问题的根本原因:

前言:这是 MariaDB,Django 1.11。

当您注释查询时,它会获得一个带有您选择的字段的GROUP BY 子句(基本上是您的values() 查询选择中的内容)。在使用 MariaDB 命令行工具调查为什么我在查询结果中得到 NULLs 或 Nones 之后,我得出的结论是 GROUP BY 子句将导致 COUNT() 返回 @987654328 @s.

然后,我开始潜入QuerySet界面,看看如何手动,从数据库查询中强行删除GROUP BY,并想出了以下代码:

from django.db.models.fields import PositiveIntegerField

reserved_seats_qs = SeatReservation.objects.filter(
        performance=OuterRef(name='pk'), status__in=TAKEN_TYPES
    ).values('id').annotate(
        count=Count('id')).values('count')
# Query workaround: remove GROUP BY from subquery. Test this
# vigorously!
reserved_seats_qs.query.group_by = []

performances_qs = Performance.objects.annotate(
    reserved_seats=Subquery(
        queryset=reserved_seats_qs,
        output_field=PositiveIntegerField()))

print(performances_qs[0].reserved_seats)

所以基本上,您必须手动删除/更新子查询查询集上的group_by 字段,以使其在执行时不会附加GROUP BY。此外,您必须指定子查询将具有的输出字段,因为 Django 似乎无法自动识别它,并在第一次评估查询集时引发异常。有趣的是,没有它,第二次评估成功。

我相信这是一个 Django 错误,或者子查询效率低下。我将创建一个关于它的错误报告。

编辑:the bug report is here。

【讨论】:

  • 您可以编写Subquery 的自定义子类,这样您就可以避免.values().annotate().values() 的舞蹈。例如:class Count(Subquery): template = "SELECT count(*) FROM (%(subquery)s) _count"
  • 这是一个有效的解决方案,谢谢。我的目标是保持在 ORM 级别并尽可能独立于 SQL。因此,group_by 调整。
  • 我花了几个小时徒劳地试图兼顾 annotate()s 和 values()s 以聚合嵌套的子查询(仅当 它们的 相关对象满足某些参数时才计数对象__in 以及来自@MatthewSchinckel 的解决方案是唯一对我有用的解决方案。我相信它应该有更多的知名度。
  • 此解决方案修复了我的 Count 子查询。很高兴我试了一下,谢谢。 Django 以一种没有帮助的方式将 GROUP BY 子句放入我的输出查询中。正如其他人所建议的那样,您可以使用子类完全避免 ORM 的输出 SQL。
【解决方案4】:

可以使用 Django 2.0 中的 Window 类来实现适用于任何一般聚合的解决方案。我也将此添加到 Django 跟踪票证中。

这允许通过基于外部查询模型(在 GROUP BY 子句中)计算分区上的聚合来聚合注释值,然后将该数据注释到子查询查询集中的每一行。然后子查询可以使用返回的第一行中的聚合数据并忽略其他行。

Performance.objects.annotate(
    reserved_seats=Subquery(
        SeatReservation.objects.filter(
            performance=OuterRef(name='pk'),
            status__in=TAKEN_TYPES,
        ).annotate(
            reserved_seat_count=Window(
                expression=Count('pk'),
                partition_by=[F('performance')]
            ),
        ).values('reserved_seat_count')[:1],
        output_field=FloatField()
    )
)

【讨论】:

  • IIRC 并非所有数据库都支持窗口函数(并且可能并非所有聚合函数都可以用作窗口函数)。话虽如此,window 的东西是一个很好的补充,我期待有一个项目使用支持它的 django 版本!
【解决方案5】:

如果我理解正确,您正在尝试计算 Carpark 中可用的 Spaces。子查询似乎有点矫枉过正,好的旧注释单独应该可以解决问题:

Carpark.objects.annotate(Count('spaces'))

这将在您的结果中包含一个spaces__count 值。


好的,我看到了你的笔记...

我还能够使用我手头的其他模型运行相同的查询。结果是一样的,所以你的例子中的查询似乎没问题(用 Django 1.11b1 测试):

activities = Activity.objects.filter(event=OuterRef('pk')).values('event')
count_activities = activities.annotate(c=Count('*')).values('c')
Event.objects.annotate(spaces__count=Subquery(count_activities))

也许你的“最简单的现实世界的例子”太简单了……你能分享模型或其他信息吗?

【讨论】:

  • 您实际上在这里提出了一个非常好的观点(我认为我们可能还没有在 django 文档中充分说明这一点):子查询将在以下情况下表现不佳通过 .annotate() 进行连接+聚合将起作用。通常,我在需要引用或聚合相关行的子集时使用它们。
【解决方案6】:

问题

问题是 Django 一看到就使用聚合函数添加GROUP BY。

解决方案

所以你可以创建自己的聚合函数,但让 Django 认为它不是聚合的。就像这样:

total_comments = Comment.objects.filter(
    post=OuterRef('pk')
).order_by().annotate(
    total=Func(F('length'), function='SUM')
).values('total')

Post.objects.filter(length__gt=Subquery(total_comments))

这样你就可以得到这样的 SQL 查询:

SELECT "testapp_post"."id", "testapp_post"."length"
FROM "testapp_post"
WHERE "testapp_post"."length" > (SELECT SUM(U0."length") AS "total"
                                 FROM "testapp_comment" U0
                                 WHERE U0."post_id" = "testapp_post"."id")

因此您甚至可以在聚合函数中使用聚合子查询。

示例

您可以统计两个日期之间的工作日数,不包括周末和节假日,并按员工汇总汇总:

class NonWorkDay(models.Model):
    date = DateField()

class WorkPeriod(models.Model):
    employee = models.ForeignKey(User, on_delete=models.CASCADE)
    start_date = DateField()
    end_date = DateField()

number_of_non_work_days = NonWorkDay.objects.filter(
    date__gte=OuterRef('start_date'),
    date__lte=OuterRef('end_date'),
).annotate(
    cnt=Func('id', function='COUNT')
).values('cnt')

WorkPeriod.objects.values('employee').order_by().annotate(
    number_of_word_days=Sum(F('end_date__year') - F('start_date__year') - number_of_non_work_days)
)

希望这会有所帮助!

【讨论】:

    【解决方案7】:

    “为我工作”没有多大帮助。但。 我在一些我方便的模型(Book -> Author 类型)上尝试了你的示例,它在 django 1.11b1 中对我来说很好。

    您确定在正确版本的 Django 中运行它吗?这是您正在运行的实际代码吗?你实际上不是在carpark 上测试这个,而是在一些更复杂的模型上测试?

    也许尝试print(thequery.query) 看看它试图在数据库中运行什么SQL。以下是我的模型(根据您的问题进行了编辑):

    SELECT (SELECT COUNT(U0."id") AS "c"
    FROM "carparks_spaces" U0
    WHERE U0."carpark_id" = ("carparks_carpark"."id")
    GROUP BY U0."carpark_id") AS "space_count" FROM "carparks_carpark"
    

    不是一个真正的答案,但希望它有所帮助。

    【讨论】:

    • 好的提示,感谢已知良好的 SQL。这实际上突出了一些额外的东西被添加到我的查询中(子查询中的一个额外的分组),但我仍然不确定为什么。我将使用非法 SQL 编辑我的问题。
    • 奖励这个赏金是因为它让我走上了帮助自己的道路,但接受了我自己的答案,因为这最有可能帮助下一个人。
    • @Oli,看看我的解决方案,我实际上正在与 django 开发人员讨论它。它与order_by() 子句无关。
    • @karolyi 这可能是对潜在问题的间接修复,但覆盖模型指定的排序确实为我解决了这个问题。额外的GROUP BY 是由多余的排序引起的。
    猜你喜欢
    • 2020-05-23
    • 2019-08-03
    • 1970-01-01
    • 2020-11-26
    • 2023-03-29
    • 2016-11-18
    • 1970-01-01
    • 1970-01-01
    • 2020-01-18
    相关资源
    最近更新 更多