Django 1.11注释子查询聚合

Oli*_*Oli 28 django django-aggregation django-annotate django-subquery

这是一个前沿的功能,我目前正在解决这个问题并迅速流血.我想在现有的查询集上注释子查询聚合.在1.11之前执行此操作要么意味着自定义SQL,要么锤击数据库.这是这方面的文档,以及它的示例:

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))
Run Code Online (Sandbox Code Playgroud)

他们在总体上注释,这对我来说似乎很奇怪,但无论如何.

我正在努力解决这个问题,所以我正在把它煮回来,回到我有数据的最简单的现实世界的例子.我有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))
Run Code Online (Sandbox Code Playgroud)

这给了我一个可爱的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";
Run Code Online (Sandbox Code Playgroud)

我突出了它,但它是子查询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
Run Code Online (Sandbox Code Playgroud)

编辑3:好的!这两个模型都有排序顺序.这些正在传递到子查询.正是这些订单膨胀了我的查询并打破了它.

我想这可能是在Django一个错误,但短于这两种模式移除元ORDER_BY的,有没有什么办法可以不排序在querytime查询?


*我知道我可以为这个例子注释一个Count.我使用它的真正目的是一个更复杂的过滤器计数,但我甚至无法实现这一点.

Oli*_*Oli 40

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

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

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))
Run Code Online (Sandbox Code Playgroud)

这很有效.雄伟.很烦人.

  • 事实证明,“Subquery”的“Exists”子类删除了排序(这实际上是出于性能原因,因为没有理由对子查询进行排序)。这是(非常少量)记录的,但也许“子查询”的文档也可以改进。 (2认同)
  • 感谢您对这篇文章.关于Django 1.11中引入的子查询,仍然没有足够的适当文档.这是迄今为止最好的问题解决者. (2认同)

Mat*_*kel 32

也可以创建一个子类Subquery,改变它输出的SQL.例如,您可以使用:

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

然后就像使用原始Subquery类一样使用它:

spaces = Space.objects.filter(carpark=OuterRef('pk')).values('pk')
Carpark.objects.annotate(space_count=SQCount(spaces))
Run Code Online (Sandbox Code Playgroud)

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


Sla*_*ava 20

问题

问题是 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))
Run Code Online (Sandbox Code Playgroud)

这样你就可以得到如下的 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")
Run Code Online (Sandbox Code Playgroud)

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

例子

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

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)
)
Run Code Online (Sandbox Code Playgroud)

希望这会有所帮助!


kar*_*lyi 11

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

前言:这是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)
Run Code Online (Sandbox Code Playgroud)

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

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

编辑:错误报告在这里.


小智 7

可以使用WindowDjango 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()
    )
)
Run Code Online (Sandbox Code Playgroud)


eil*_*rra 5

如果我理解正确,您正在尝试计算Space中可用的 s Carpark。子查询对此似乎有些过分了,仅靠旧的注释可以解决问题:

Carpark.objects.annotate(Count('spaces'))
Run Code Online (Sandbox Code Playgroud)

这将在您的结果中包含一个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))
Run Code Online (Sandbox Code Playgroud)

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