51

这是一个前沿功能,我目前正在使用它并很快就流血了。我想将子查询聚合注释到现有查询集上。在 1.11 之前执行此操作意味着自定义 SQL 或锤击数据库。这是 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))

他们在汇总上进行注释,这对我来说似乎很奇怪,但无论如何。

我正在为此苦苦挣扎,所以我将其直接还原为我拥有数据的最简单的真实示例。我有Carpark包含许多Spaces 的 s。如果这让你更快乐,请使用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,我就可以通过 ☹</p> 看到第二组

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 注释。我使用它的真正目的是一个更复杂的过滤器计数,但我什至无法让它工作。

4

7 回答 7

66

沙赞!根据我的编辑,我的子查询正在输出一个额外的列。这是为了方便订购(在 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))

那行得通。太棒了。很烦人。

于 2017-03-07T12:58:54.877 回答
60

也可以创建一个 的子类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 中):我经常用它来构建一个值数组,或者对它们求和。

于 2017-11-18T22:00:32.420 回答
16

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

前言:这是 MariaDB,Django 1.11。

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

然后,我开始深入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 错误,或者子查询效率低下。我将创建一个关于它的错误报告。

编辑:错误报告在这里

于 2017-06-10T14:09:31.353 回答
7

Window可以使用Django 2.0 中的类来实现适用于任何一般聚合的解决方案。我也将此添加到 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()
    )
)
于 2018-07-22T18:50:02.777 回答
5

问题

问题是 DjangoGROUP 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)
)

希望这会有所帮助!

于 2021-09-01T21:26:39.697 回答
4

如果我理解正确,您正在尝试计算a 中Space可用的 s Carpark。子查询似乎有点过头了,好的旧注释本身就可以解决问题:

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

也许您的“最简单的真实示例”太简单了……您可以分享模型或其他信息吗?

于 2017-03-04T15:00:47.800 回答
2

“为我工作”并没有太大帮助。但。我在一些我方便的模型(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"

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

于 2017-03-04T16:55:37.523 回答