Django Аннотированный запрос для подсчета только последних из обратных отношений - PullRequest
1 голос
/ 06 марта 2020

Обзор проблемы

С учетом моделей

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 вопросы по агрегации, но я не смог найти четкого ответа по поводу агрегирования и аннотирования "последнее.

Заранее спасибо.

1 Ответ

1 голос
/ 07 марта 2020

Мы можем выполнить запрос, в котором мы сначала фильтруем последние StatusChange с на Candidate, а затем подсчитываем статусы:

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)]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...