Django 1.11注释子查询聚合

Django 1.11 Annotating a Subquery Aggregate

这是我目前最喜欢使用的一项前沿功能,并且很快就会消失。我想将子查询聚合注释到现有查询集上。在1.11之前执行此操作意味着自定义SQL或修改数据库。这是此文档以及其中的示例:

1
2
3
4
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,其中包含许多Space。使用Book→Author可以使您更快乐,但是-现在-我只想使用Subquery *注释相关模型的数量。

1
2
3
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,并尝试使其更像他们在答案中发布的版本。这是我发现真正问题的地方:

1
2
3
4
5
6
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,我就可以看到第二组吗?

1
2
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。我使用此功能的真正目的是要使用更复杂的过滤器计数,但我什至无法使它正常工作。


!根据我的修改,正在从我的子查询输出另外一列。这是为了方便订购(COUNT不需要)。

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

1
2
3
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))

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


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

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

然后,您将像使用原始Subquery类一样使用它:

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

您可以将此技巧(至少在postgres中)与一系列聚合函数结合使用:我经常使用它来构建值数组或求和。


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

前言:这是MariaDB,Django 1.11。

注释查询时,它会获得一个带有所选字段的GROUP BY子句(基本上是values()查询选择中的内容)。在研究了MariaDB命令行工具后,为什么在查询结果中得到了NULLNone,我得出的结论是GROUP BY子句将导致COUNT()返回 s。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

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

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

编辑:错误报告在这里。


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()
    )
)


如果我理解正确,您正在尝试计算Carpark中可用的Space。子查询对此似乎有些矫kill过正,只有好的旧注释才可以解决问题:

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

结果中将包含一个spaces__count值。

好,我看过你的纸条...

我还可以使用我手头的其他模型来运行相同的查询。结果是相同的,因此示例中的查询似乎还可以(使用Django 1.11b1测试):

1
2
3
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))

也许您的"最简单的现实世界示例"太简单了……您可以共享模型或其他信息吗?


"为我工作"并没有太大帮助。但。
我在一些方便的型号(Book -> Author类型)上尝试了您的示例,在Django 1.11b1中对我来说效果很好。

您确定您在正确版本的Django中运行此代码吗?这是您正在运行的实际代码吗?您是否实际上不是在Carpark上而是在更复杂的模型上进行测试?

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

1
2
3
4
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"

并非真正的答案,但希望能有所帮助。