通过 ForeignKey 外部引用过滤的汇总子查询注释

Posted

技术标签:

【中文标题】通过 ForeignKey 外部引用过滤的汇总子查询注释【英文标题】:Summarized Subquery annotation with a filter by ForeignKey outer reference 【发布时间】:2020-10-17 08:41:57 【问题描述】:

我尝试从这个 SQL 查询中编写等效的 Django 查询,但我被卡住了。 欢迎任何帮助。 我收到了一场比赛id,我想从这场比赛中做一些统计:nb_race = 比赛前一匹马的比赛次数,best_chrono = 比赛前一匹马的最佳时间。

SELECT *, (SELECT count(run.id)
                FROM runner run
                INNER JOIN race
                ON run.race_id = race.id
                WHERE run.horse_id = r.horse_id
                AND race.datetime_start < rc.datetime_start 
                ) AS nb_race, 
          (SELECT min(run.chrono)
                FROM runner run
                INNER JOIN race
                ON run.race_id = race.id
                WHERE run.horse_id = r.horse_id
                AND race.datetime_start < rc.datetime_start 
                ) AS best_time
FROM runner r, race rc
WHERE r.race_id = rc.id
AND rc.id = 7890

Django 模型:

class Horse(models.Model):
    id = AutoField(primary_key=True)
    name = models.CharField(max_length=255, blank=True, null=True, default=None)

class Race(models.Model):
    id = AutoField(primary_key=True)
    datetime_start = models.DateTimeField(blank=True, null=True, default=None)
    name = models.CharField(max_length=255, blank=True, null=True, default=None)

class Runner(models.Model):
    id = AutoField(primary_key=True)
    horse = models.ForeignKey(Horse, on_delete=models.PROTECT)
    race = models.ForeignKey(Race, on_delete=models.PROTECT)
    chrono = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, default=None)

【问题讨论】:

由于查询的复杂性,您可能需要考虑通过 Django 的 raw() 功能运行此查询。有关该主题的信息可以在 Django 网站上找到。docs.djangoproject.com/en/3.0/topics/db/sql 我确实如此,但我不明白“相关子查询”在 Django 中是如何工作的。我的最后一次尝试是这样的:runners = Runner.objects.filter(race_id=race.id) \ .annotate( nb_course=RawSQL( "SELECT count(runner.id) FROM runner INNER JOIN race ON race.course_id = race.id WHERE runner.horse_id = %s", (OuterRef('horse_id')))) 我只是无法访问“Runner.objects.filter(race_id=race.id)”中的“horse_id” Frederic,您使用哪种尺寸的桌子?好像您使用的是一个相当小的数据集,效率不是那么重要,可能值得将您的主要数据提取到 python 并使用 pandas 来应用聚合和查询?只是一个建议。 我的 db 是随之而来的:12 000 000 名跑步者,990 000 匹马。这只是 20 多个表中的 2 个……总共大约 10 GiB。不认识pandas,我去看看。 是的,坚持使用 SQL。我明天去试试。 【参考方案1】:

Subquery expression 可用于将附加查询集编译为依赖于主查询集的子查询,并将它们作为一个 SQL 一起执行。

from django.db.models import OuterRef, Subquery, Count, Min, F

# prepare a repeated expression about previous runners, but don't execute it yet
prev_run = (
    Runner.objects
    .filter(
        horse=OuterRef('horse'),
        race__datetime_start__lt=OuterRef('race__datetime_start'))
    .values('horse')
)
queryset = (
    Runner.objects
    .values('id', 'horse_id', 'race_id', 'chrono', 'race__name', 'race__datetime_start')
    .annotate(
        nb_race=Subquery(prev_run.annotate(nb_race=Count('id')).values('nb_race')),
        best_time=Subquery(prev_run.annotate(best_time=Min('chrono')).values('best_time'))
    )
)

此处使用的一些技巧在链接文档中进行了描述:

子查询的输出字段必须由.values(...) 限制为一个字段:仅聚合值 子查询必须是一个查询集(被惰性计算并组合在一起),而不是一个值(将立即计算并失败)。因此在子查询中使用.annotate()(不是.aggregate())。这增加了一个GROUP BY race.horse_id,但这不是问题,因为还有WHERE race.horse_id = ...,并且“group by”最终将被现代数据库后端中的 SQL 优化器忽略。

它被编译为与示例中的 SQL 等效的查询。检查 SQL:

>>> print(str(queryset.query))
SELECT ...,
  (SELECT COUNT(U0.id)
   FROM runner U0 INNER JOIN race U1 ON (U0.race_id = U1.id)
   WHERE (U0.horse_id = runner.horse_id AND U1.datetime_start < race.datetime_start)
   GROUP BY U0.horse_id
   ) AS nb_race,
   ...
FROM runner INNER JOIN race ON (runner.race_id = race.id)

一个微小的区别是子查询使用一些内部别名,如 U0 和 U1。

【讨论】:

这行得通,我喜欢prev_run 方法,这大大简化了其余代码。谢谢你hynekcer。 如果您同意,我现在将重命名您的问题,以便其他有类似问题的人更容易搜索。 当然,我同意,重命名标题。

以上是关于通过 ForeignKey 外部引用过滤的汇总子查询注释的主要内容,如果未能解决你的问题,请参考以下文章

按 foreignKey 关系中的字段过滤

是否可以使用 findAll() 创建查询并通过使用来自 pivot 的 ForeignKey(关系多对多)获得过滤结果?

推荐算法-协同过滤代码问题汇总

推荐算法-协同过滤代码问题汇总

如何使用 DjangoFilterBackend 过滤 ForeignKey 的多个值

使用 Q 对象过滤多个 ForeignKey 匹配