Django 1.11注释子查询聚合

时间:2017-03-01 23:18:22

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

这是一个前沿功能,我目前正在努力解决并迅速流血。我想在现有的查询集上注释子查询聚合。在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))

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

我正在努力解决这个问题,所以我正在把它归结为我有数据的最简单的现实世界的例子。我有Carpark个,其中包含许多Space个。使用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 。我使用它的真正目的是使用更复杂的过滤器计数,但我甚至无法实现这一点。

6 个答案:

答案 0 :(得分:32)

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

我只需要从模型中删除指定的元顺序。您可以通过向子查询添加空thread_safe_statics.cpp来完成此操作。在我的代码术语中意味着:

.order_by()

这很有效。雄伟。太烦人了。

答案 1 :(得分:26)

还可以创建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中)使用一系列聚合函数:我经常使用它来构建一个值数组,或者将它们相加。

答案 2 :(得分:10)

我刚刚碰到了一个非常类似的案例,我必须在预订状态未取消的事件中获得座位预订。在试图解决问题几个小时之后,这就是我所看到的问题的根本原因:

前言:这是MariaDB,Django 1.11。

当您对查询进行注释时,它会获得一个GROUP BY子句,其中包含您选择的字段(基本上是values()查询选择中的内容)。在使用MariaDB命令行工具进行调查后,为什么我在查询结果中获得了NULLNone s,我得出了GROUP BY子句的结论会导致COUNT()返回NULL 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

答案 3 :(得分:3)

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

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

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

答案 4 :(得分:2)

如果我理解正确,您正试图计算Space中可用的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))

也许你的"最简单的现实世界的例子"太简单了...你可以分享模型或其他信息吗?

答案 5 :(得分:1)

"适合我"没有多大帮助。但。 我在一些我很方便的模型(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"

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