Предполагая, что у меня есть следующие модели: как я могу аннотировать общее количество публикаций и общий охват историй каждого влиятельного лица в моем наборе запросов?
class Influencer(models.Model):
name = models.CharField(max_length=100)
class Posting(models.Model):
influencer = models.ForeignKey(Influencer, on_delete=models.CASCADE)
reach = models.IntegerField()
class Story(models.Model):
influencer = models.ForeignKey(Influencer, on_delete=models.CASCADE)
reach = models.IntegerField()
Я пробовал это:
queryset = Influencer.objects.all()
queryset = queryset.annotate(posting_reach=Sum("posting__reach"))
queryset = queryset.annotate(story_reach=Sum("story__reach"))
Однако при таком подходе значения не рассчитываются правильно (я полагаю, из-за LEFT OUTER JOIN
, созданного Sum()
). Как бы я сделал это в Джанго?
Попробуйте с different=True внутри count или напишите выражения подзапроса docs.djangoproject.com/en/3.1/topics/db/aggregation/…
@iklinac: само по себе это не сработает. Например, если есть два Posting
с одинаковым количеством reach
, то вы считаете их только один раз.
@schillingt: из-за двух LEFT OUTER JOIN
они будут действовать как множители друг для друга, поскольку каждый связанный Story
будет повторяться для каждого связанного Posting
и наоборот.
Не уверен, как именно реализовано отличие внутри count, поэтому вы, вероятно, правы, хороший обходной путь без использования подзапроса.
@iklinac: обычно тот факт, что это подсчет, не имеет значения, он обычно работает как COUNT(DISTINCT posting.reach)
, поэтому сначала проходит фильтр уникальности, прежде чем передать его подсчету. Сумма не «знает», что она подсчитывает различные значения. Это делается как «предварительная обработка» перед передачей набора значений агрегату COUNT
.
Да, вы правы, немного погуглил :)
Это действительно не сработает, потому что ваш запрос будет состоять из двух LEFT OUTER JOIN
s:
SELECT influencer.*, SUM(posting.reach), SUM(story.reach)
FROM influencer
LEFT OUTER JOIN posting ON posting.influencer_id = influencer.id
LEFT OUTER JOIN story ON story.influencer_id = influencer.id
два LEFT OUTER JOIN
, таким образом, будут действовать как множители друг друга, и, таким образом, сумма posting.reach
будет умножена на количество связанных story
, и наоборот.
Хитрость может состоять в том, чтобы разделить его на количество элементов другого отношения, поэтому:
from django.db.models import Count, Sum, Value
from django.db.models.functions import Coalesce
queryset = Influencer.objects.annotate(
posting_reach=Sum('posting__reach') / Coalesce(Count('story'), Value(1)),
story_reach=Sum('story__reach') / Coalesce(Count('posting'), Value(1))
)
хорошо, это может сработать, но, честно говоря, это выглядит не очень "питоновски"
@RaideR: альтернативой является Subquery
, но это внесет еще больше «шума».
Для всех, кто заинтересован в решении подзапроса:
from django.db.models import (OuterRef, Subquery, Sum, IntegerField)
subs = {
'posting_reach': Posting.objects.filter(influencer=OuterRef('pk')),
'story_reach': Story.objects.filter(influencer=OuterRef('pk')),
}
qs = Influencer.objects.annotate(
**{key: Subquery(
sub_q.annotate(sum=Sum('reach')).values('sum'),
output_field=IntegerField()
) for key, sub_q in subs
}
)
Можете ли вы объяснить нам, почему вы говорите, что это неправильно? Кроме того, пожалуйста, используйте
print(queryset.query)
и включите это. Это может дать нам больше информации.