Использование подзапроса для аннотирования графа

Пожалуйста, помогите мне, я застрял на этом слишком долго:(

Что я хочу сделать:

У меня есть эти две модели:

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"))

Однако это не работает, потому что мне нужно посчитать конкретный набор запросов врачей.

Я перешел по этим ссылкам

Тем не менее, я не получаю тот же результат:

Если у вас есть какие-либо вопросы, которые могли бы прояснить ситуацию, пожалуйста, сообщите мне.

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 это целое число (количество врачей этой специализации).

Вы также можете отфильтровать на Specializations в том же запросе (например, получить только специализации, которые заканчиваются на 'my'). Пока вы не фильтруете doctorустановлен, то Count будет работать (см. раздел ниже, как это сделать).

Если вы, однако, фильтр на связанных doctors, то соответствующие подсчеты отфильтруют этих врачей. Кроме того, если вы отфильтруете другой связанный объект, это приведет к дополнительному JOIN, который будет действовать как множитель для Counts. В этом случае может быть лучше использовать num_doctors=Count('doctor', distinct=True) вместо. Вы всегда можете использовать distinct=True (независимо от того, если вы делаете дополнительные JOINили нет), но это будет иметь небольшое влияние на производительность.

Выше работает, потому что Count('doctor') не просто добавляет всех врачей к запросу, это делает LEFT OUTER JOIN на doctorS, и таким образом проверяет, что 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

Другие вопросы по тегам