Использование подзапроса для аннотирования графа
Пожалуйста, помогите мне, я застрял на этом слишком долго:(
Что я хочу сделать:
У меня есть эти две модели:
class Specialization(models.Model):
name = models.CharField("name", max_length=64)
class Doctor(models.Model):
name = models.CharField("name", max_length=128)
# ...
specialization = models.ForeignKey(Specialization)
Я хотел бы аннотировать все специализации в наборе запросов с количеством врачей, которые имеют эту специализацию.
Мое решение до сих пор:
Я прошел через цикл, и я сделал простое: Doctor.objects.filter(specialization=spec).count()
однако это оказалось слишком медленным и неэффективным. Чем больше я читаю, тем больше я понимаю, что имеет смысл использовать SubQuery
здесь, чтобы отфильтровать врачей для OuterRef
специализация. Вот что я придумал:
doctors = Doctor.objects.all().filter(specialization=OuterRef("id")) \
.values("specialization_id") \
.order_by()
add_doctors_count = doctors.annotate(cnt=Count("specialization_id")).values("cnt")[:1]
spec_qs_with_counts = Specialization.objects.all().annotate(
num_applicable_doctors=Subquery(add_doctors_count, output_field=IntegerField())
)
Вывод, который я получаю, составляет всего 1 для каждой специальности. Код просто аннотирует каждый объект доктора с его specialization_id
а затем аннотирует счет в этой группе, означая, что это будет 1.
Это не имеет полного смысла для меня, к сожалению. В моей первой попытке я использовал агрегат для подсчета, и хотя он работает сам по себе, он не работает как SubQuery
Я получаю эту ошибку:
This queryset contains a reference to an outer query and may only be used in a subquery.
Я разместил этот вопрос раньше, и кто-то предложил сделать Specialization.objects.annotate(count=Count("doctor"))
Однако это не работает, потому что мне нужно посчитать конкретный набор запросов врачей.
Я перешел по этим ссылкам
Тем не менее, я не получаю тот же результат:
https://docs.djangoproject.com/en/1.11/ref/models/expressions/
https://medium.com/@hansonkd/the-dramatic-benefits-of-django-subqueries-and-annotations-4195e0dafb16
Если у вас есть какие-либо вопросы, которые могли бы прояснить ситуацию, пожалуйста, сообщите мне.
2 ответа
Считать все Doctor
с за Specialization
Я думаю, что вы делаете вещи слишком сложными, вероятно, потому что вы думаете, что Count('doctor')
будет рассчитывать каждого врача по специализации (независимо от специализации этого врача). Это не так, если вы Count
такой связанный объект, Django неявно ищет связанные объекты. На самом деле вы не можете Count('unrelated_model')
вообще, это только через отношения (обратное в том числе) как ForeignKey
, ManyToManyField
и т. д., что вы можете запросить их, так как в противном случае они не очень чувственные.
Я хотел бы аннотировать все специализации в наборе запросов с количеством врачей, которые имеют эту специализацию.
Вы можете сделать это с помощью простого:
# Counting all doctors per specialization (so not all doctors in general)
from django.db.models import Count
Specialization.objects.annotate(
num_doctors=Count('doctor')
)
Теперь каждый Specialization
Объект в этом наборе запросов будет иметь дополнительный атрибут num_doctors
это целое число (количество врачей этой специализации).
Вы также можете отфильтровать на Specialization
s в том же запросе (например, получить только специализации, которые заканчиваются на 'my'
). Пока вы не фильтруете doctor
установлен, то Count
будет работать (см. раздел ниже, как это сделать).
Если вы, однако, фильтр на связанных doctor
s, то соответствующие подсчеты отфильтруют этих врачей. Кроме того, если вы отфильтруете другой связанный объект, это приведет к дополнительному JOIN
, который будет действовать как множитель для Count
s. В этом случае может быть лучше использовать num_doctors=Count('doctor', distinct=True)
вместо. Вы всегда можете использовать distinct=True
(независимо от того, если вы делаете дополнительные JOIN
или нет), но это будет иметь небольшое влияние на производительность.
Выше работает, потому что Count('doctor')
не просто добавляет всех врачей к запросу, это делает LEFT OUTER JOIN
на doctor
S, и таким образом проверяет, что specialization_id
того, что Doctor
это именно то, что мы ищем. Таким образом, запрос, который Django создаст, выглядит так:
SELECT specialization.*
COUNT(doctor.id) AS num_doctors
FROM specialization
LEFT OUTER JOIN doctor ON doctor.specialization_id = specialization.id
GROUP BY specialization.id
Делая то же самое с подзапросом, вы получите функциональные результаты, но если Django ORM и система управления базами данных не найдут способ оптимизировать это, это может привести к дорогостоящему запросу, поскольку для каждой специализации это может привести к дополнительный подзапрос в базе данных.
Подсчет конкретных Doctor
с за Specialization
Скажем, однако, что вы хотите считать только врачей, чье имя начинается с Джо, тогда вы можете добавить фильтр на связанный doctor
, лайк:
# counting all Doctors with as name Joe per specialization
from django.db.models import Count
Specialization.objects.filter(
doctor__name__startswith='Joe' # sample filter
).annotate(
num_doctors=Count('doctor')
)
Сделать это можно так:
doctors = Doctor.objects.filter(
specialization=OuterRef("id")
).order_by().annotate(
count=Func('id', 'Count')
).values('count')
spec_qs_with_counts = Specialization.objects.annotate(
num_applicable_doctors=Subquery(doctors)
)
Более подробную информацию об этом методе вы можете увидеть в этом ответе: /questions/8884513/django-111-annotirovanie-agregata-podzaprosa/58949886#58949886