【问题标题】:Django Annotated Query to Count Only Latest from Reverse RelationshipDjango Annotated Query 仅计算反向关系中的最新查询
【发布时间】:2020-06-19 13:04:27
【问题描述】:

问题概述

给定模型

class Candidate(BaseModel):
    name = models.CharField(max_length=128)

class Status(BaseModel):
    name = models.CharField(max_length=128)

class StatusChange(BaseModel):
    candidate = models.ForeignKey("Candidate", related_name="status_changes")
    status = models.ForeignKey("Status", related_name="status_changes")
    created_at = models.DateTimeField(auto_now_add=True, blank=True)

和 SQL 表:

candidates
+----+--------------+
| id | name         |
+----+--------------+
|  1 | Beth         |
|  2 | Mark         |
|  3 | Mike         |
|  4 | Ryan         |
+----+--------------+

status
+----+--------------+
| id | name         |
+----+--------------+
|  1 | Review       |
|  2 | Accepted     |
|  3 | Rejected     |
+----+--------------+

status_change
+----+--------------+-----------+------------+
| id | candidate_id | status_id | created_at |
+----+--------------+-----------+------------+
|  1 | 1            | 1         | 03-01-2019 |
|  2 | 1            | 2         | 05-01-2019 |
|  4 | 2            | 1         | 01-01-2019 |
|  5 | 3            | 1         | 01-01-2019 |
|  6 | 4            | 3         | 01-01-2019 |
+----+--------------+-----------+------------+

我想获取具有给定状态的候选人总数,但只计算最新的status_change

换句话说,StatusChange 用于跟踪状态的历史记录,但在计算候选人的当前状态时只考虑最新的。

SQL 解决方案

使用 SQL,我能够使用 Group BY 和 COUNT 实现它。 (未经测试的 SQL)

SELECT
       status.id as status_id
    ,  status.name as status_name
    , COUNT(*) as status_count
FROM
    (
    SELECT
        status_id, 
        Max(created_at) AS latest_status_change
    FROM 
        status_change
    GROUP BY status_id
    ) 
AS last_status_count
INNER JOIN 
    last_status_count AS status 
    ON (last_status_count.status_id = status.id)
GROUP BY status.name
ORDER BY status_count DESC;
last_status_count
+-----------+-------------+--------+
| status_id | status_name | count  |
+-----------+-------------+--------+
| 1         | Review      | 2      | # <= Does not include instance from candidate 1
| 2         | Accepted    | 1      | # because status 2 is latest
| 3         | Rejected    | 1      |
+-----------+-------------+--------+

尝试过的 Django 解决方案

我需要一个视图来返回每个状态及其相应的计数 - 例如[{ status_name: "Review", count: 2 }, ...]

我不确定如何构建这个查询集,而不是在 python 中提取所有记录和聚合。

我认为我需要 annotate() 和可能的 Subquery,但我无法将它们拼接在一起。

我得到的最接近的是这个,它计算每个状态的状态更改次数,但计算非最新更改。

    queryset = Status.objects.all().annotate(case_count=Count("status_changes"))

我发现了很多关于聚合的 SO 问题,但我找不到关于聚合和注释“最新”的明确答案。

提前致谢。

【问题讨论】:

  • 同一个candidate 有可能有两个(或多个StatusChanges 和同一个create_id)?
  • 好问题。理论上是的,但是完全匹配是不可能的,因为它们带有时间戳(日期时间)。我也可以使用id col 来“排序”并定义最新的而不是created_at。 (PS:抱歉打错了,栏目应该是created_at

标签: python mysql django django-queryset django-orm


【解决方案1】:

我们可以执行查询,首先过滤每个Candidate 的最后一个StatusChanges,然后计算状态:

from django.db.models import Count, F, Max

Status.objects.filter(
    status_changes__in=StatusChange.objects.annotate(
        last=Max('candidate__status_changes__created_at')
    ).filter(
        created_at=F('last')
    )
).annotate(
    nlast=Count('status_changes')
)

对于给定的样本数据,这给了我们:

>>> [(q.name, q.nlast) for q in qs]
[('Review', 2), ('Accepted', 1), ('Rejected', 1)]

【讨论】:

  • 感谢您的回答@willem-van-onsem,这是一个巨大的帮助。后续问题 - 如果在status_change 中未“使用”某个状态,它最终会从结果中省略。如何在此查询之上进行外部联接,以便没有更改的状态显示 zero 计数?
  • 跟进此处记录的问题 - stackoverflow.com/questions/60627961/…
猜你喜欢
  • 1970-01-01
  • 2019-05-18
  • 2022-01-22
  • 1970-01-01
  • 2011-03-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多