Django 1.11 注释子查询聚合
Django 1.11 Annotating a Subquery Aggregate
这是一个前沿功能,我目前一直在使用它,但很快就会流血。我想将子查询聚合注释到现有查询集上。在 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
s,其中包含许多 Space
s。如果 Book→Author
让您更快乐,请使用 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...
添加一些内容
我构建了一个新的 Carpark/Space 模型并且它起作用了。所以下一步是弄清楚是什么毒化了我的 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"<strong>, U0."space"</strong>
)
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,有什么方法可以 unsort 查询时的查询?
*我知道我可以为这个例子注释一个计数。我使用它的真正目的是一个更复杂的过滤器计数,但我什至无法让它工作。
如果我没理解错的话,您是在计算 Carpark
中可用的 Space
个。子查询对此似乎有些矫枉过正,好的旧注释 单独 应该可以解决问题:
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))
可能是你的"simplest real-world example"太简单了...能分享一下模型或者其他信息吗?
"works for me" 帮助不大。但。
我在手边的一些模型(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"
不是真正的答案,但希望它能有所帮助。
沙赞!根据我的编辑,我的子查询输出了一个额外的列。这是为了方便订购(这在 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))
这行得通。棒极了。太烦人了
我刚刚遇到了一个非常相似的案例,我必须为未取消预订状态的活动预订座位。在尝试找出问题几个小时后,我认为这是问题的根本原因:
前言:这是 MariaDB,Django 1.11。
当您注释一个查询时,它会得到一个 GROUP BY
子句,其中包含您 select 的字段(基本上是您的 values()
查询 selection 中的内容)。在使用 MariaDB 命令行工具调查了为什么我在查询结果中得到 NULL
s 或 None
s 之后,我得出的结论是 GROUP BY
子句将导致 COUNT()
到 return 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)
所以基本上,您必须手动 remove/update 子查询查询集中的 group_by
字段,以使其在执行时不附加 GROUP BY
。此外,您还必须指定子查询将具有的输出字段,因为 Django 似乎无法自动识别它,并在查询集的第一次评估时引发异常。有趣的是,没有它,第二次评估成功了。
我认为这是一个 Django 错误,或者子查询效率低下。我将创建一个关于它的错误报告。
也可以创建 Subquery
的子 class,改变它输出的 SQL。例如,您可以使用:
class SQCount(Subquery):
template = "(SELECT count(*) FROM (%(subquery)s) _count)"
output_field = models.IntegerField()
然后您可以像原来一样使用它 Subquery
class:
spaces = Space.objects.filter(carpark=OuterRef('pk')).values('pk')
Carpark.objects.annotate(space_count=SQCount(spaces))
您可以将这个技巧(至少在 postgres 中)与一系列聚合函数一起使用:我经常用它来构建一个值数组,或者对它们求和。
可以使用 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()
)
)
问题
问题是 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)
)
希望这会有所帮助!
这是一个前沿功能,我目前一直在使用它,但很快就会流血。我想将子查询聚合注释到现有查询集上。在 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
s,其中包含许多 Space
s。如果 Book→Author
让您更快乐,请使用 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...
添加一些内容我构建了一个新的 Carpark/Space 模型并且它起作用了。所以下一步是弄清楚是什么毒化了我的 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"<strong>, U0."space"</strong>
)
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,有什么方法可以 unsort 查询时的查询?
*我知道我可以为这个例子注释一个计数。我使用它的真正目的是一个更复杂的过滤器计数,但我什至无法让它工作。
如果我没理解错的话,您是在计算 Carpark
中可用的 Space
个。子查询对此似乎有些矫枉过正,好的旧注释 单独 应该可以解决问题:
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))
可能是你的"simplest real-world example"太简单了...能分享一下模型或者其他信息吗?
"works for me" 帮助不大。但。
我在手边的一些模型(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"
不是真正的答案,但希望它能有所帮助。
沙赞!根据我的编辑,我的子查询输出了一个额外的列。这是为了方便订购(这在 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))
这行得通。棒极了。太烦人了
我刚刚遇到了一个非常相似的案例,我必须为未取消预订状态的活动预订座位。在尝试找出问题几个小时后,我认为这是问题的根本原因:
前言:这是 MariaDB,Django 1.11。
当您注释一个查询时,它会得到一个 GROUP BY
子句,其中包含您 select 的字段(基本上是您的 values()
查询 selection 中的内容)。在使用 MariaDB 命令行工具调查了为什么我在查询结果中得到 NULL
s 或 None
s 之后,我得出的结论是 GROUP BY
子句将导致 COUNT()
到 return 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)
所以基本上,您必须手动 remove/update 子查询查询集中的 group_by
字段,以使其在执行时不附加 GROUP BY
。此外,您还必须指定子查询将具有的输出字段,因为 Django 似乎无法自动识别它,并在查询集的第一次评估时引发异常。有趣的是,没有它,第二次评估成功了。
我认为这是一个 Django 错误,或者子查询效率低下。我将创建一个关于它的错误报告。
也可以创建 Subquery
的子 class,改变它输出的 SQL。例如,您可以使用:
class SQCount(Subquery):
template = "(SELECT count(*) FROM (%(subquery)s) _count)"
output_field = models.IntegerField()
然后您可以像原来一样使用它 Subquery
class:
spaces = Space.objects.filter(carpark=OuterRef('pk')).values('pk')
Carpark.objects.annotate(space_count=SQCount(spaces))
您可以将这个技巧(至少在 postgres 中)与一系列聚合函数一起使用:我经常用它来构建一个值数组,或者对它们求和。
可以使用 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()
)
)
问题
问题是 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)
)
希望这会有所帮助!