Проверьте, встречается ли object_id более одного раза в наборе запросов.аннотировать случай, когда параметр

#python #django #django-1.11

#python #django #django-1.11

Вопрос:

Поиск поля документации в моем случае не очень помогает

Как выглядит мой запрос сейчас

 date_delta = 2

queryset = TrendData.objects.filter(owner__trend_type__mnemonic='posts', 
 date_trend__date__range=[date_from, date_to]).values('owner_id', 'owner__name')

queryset.annotate(owner_name=F('owner_id__name')).values('owner_name', 'owner_id').annotate(
    views = Sum(Case(When(owner_id__gt=1, then=F('views') / date_delta)), default=('views')...,
                output_field=IntegerField() )
)
 

queryset вывод выглядит следующим образом:

 {'owner_id': 1306, 'owner__name': 'Some name123'}, 
{'owner_id': 1307, 'owner__name': 'Somename as well'}, 
{'owner_id': 1308, 'owner__name': 'aand another name'}, 
{'owner_id': 1306, 'owner__name': 'Some name123'}
 

как вы можете видеть, есть совпадающие идентификаторы owner_id, а набор запросов len() составляет 100 тыс. в день, поэтому, если диапазон дат составляет 5 дней, набор запросов len() == 500 тыс.
мой models.py посмотрите вот так

 class Owner(models.Model):
    class Meta:
        verbose_name_plural = 'Objects'

    TREND_OWNERS = Choices('group', 'user')

    link = models.CharField(max_length=255)
    name = models.CharField(max_length=255)
    owner_type = models.CharField(choices=TREND_OWNERS, max_length=50)
    trend_type = models.ForeignKey(TrendType, on_delete=models.CASCADE)

    def __str__(self):
        return f'{self.link}[{self.trend_type}]'


class TrendData(models.Model):
    class Meta:
        verbose_name_plural = 'Trends'

    owner = models.ForeignKey(Owner, on_delete=models.CASCADE)
    views = models.IntegerField()
    views_u = models.IntegerField()
    likes = models.IntegerField()
    shares = models.IntegerField()
    interaction_rate = models.DecimalField(max_digits=20, decimal_places=10)
    mean_age = models.IntegerField()
    date_trend = models.DateTimeField()
 

Я понял, что это будет работать нормально, но это будет неправильно, поскольку, если owner_id велик, он будет делиться на date_delta , где в моем случае я хочу, чтобы owner_id встречался в наборе запросов более одного раза. Я пробовал owner_id__count__gt , но этого не существует:(

Я хотел бы знать, есть ли способ подсчитать вхождение owner_id в моем Case(When()) наборе запросов annotate. это буквально решит мою проблему. если оно больше 1, чем мы делим на date_delta , иначе мы оставляем его как есть

Обновить:

Просто для ясности, эта аннотация отлично справляется с задачей, однако она также разделяет некоторые запросы, которые я не хочу разделять (в моем случае НЕ дублирующий набор запросов owner_id по-прежнему делит его просмотры, общие ресурсы и т. Д. На 2), Поэтому я использую Case(When()), упомянутый выше

 queryset.values('owner__name', 'owner_id').annotate(
    views=Sum('views') / 2, 
    views_u=Sum('views_u') / 2, 
    likes=Sum('likes') / 2,
    shares=Sum('shares') / 2, 
    interaction_rate=Sum('interaction_rate') / 2,
    mean_age=Sum('mean_age') / 2)
 

ОБНОВЛЕНИЕ # 2
Это моя логика, но в python

 json_output = []
for item in (queryset
                .values('owner__name', 'owner_id')
                .annotate(owner_count=Count('owner_id'))
                .annotate(views=Sum('views'), views_u=Sum('views_u'),
                            likes=Sum('likes'),
                            shares=Sum('shares'),
                            interaction_rate=Sum('interaction_rate'),
                            mean_age=Sum('mean_age')):
    if item['owner_count'] > 1:
        item['views'] = item['views'] / date_delta
        item['views_u'] = item['views_u'] / date_delta
        item['likes'] = item['likes'] / date_delta
        item['shares'] = item['shares'] / date_delta
        item['interaction_rate'] = '{:.10f}'.format(
            Decimal(item['interaction_rate']) / date_delta)
        item['mean_age'] = item['mean_age'] / date_delta
        json_output.append(item)
    else:
        json_output.append(item)
 

Комментарии:

1. Какова конечная цель этого запроса? владельцы с более чем одним трендом данных?

2. @Sayse конечная цель — суммировать все представления trenddata.objects, и если в этих объектах более 1 владельца, разделите его на date_delta(2 в нашем случае), если нет, просто оставьте сумму такой, какая она есть

3. Никогда не будет более одного владельца, поскольку это внешний ключ, а не отношение «многие ко многим»

4. Мой набор запросов (TrendData) — это диапазон дат пример: <QuerySet [{'id': 275369, 'owner_id': 155116, 'views': 19882, 'views_u': 13351, 'likes': 71, 'shares': 5, 'interaction_rate': Decimal('0.005692457300000'), 'mean_age': 31, 'source_id': 3, 'date_trend': datetime.datetime(2019, 4, 6, 0, 0, tzinfo=<UTC>)}, {'id': 275370, 'owner_id': 155116, 'views': 15280, 'views_u': 13351, 'likes': 160, 'shares': 10, 'interaction_rate': Decimal('0.012733128900000'), 'mean_age': 32, 'source_id': 3, 'date_trend': datetime.datetime(2019, 4, 5, 0, 0, tzinfo=<UTC>)} в 1 наборе запросов более 2 владельцев

5. Итак, вы ищете агрегацию представлений, а не аннотацию?

Ответ №1:

Обновление: оказывается, я все-таки не протестировал это полностью (я думал, что у меня есть, извинения). Вам нужно Case обернуть вокруг Sum , наоборот ( Sum вокруг Case ) не будет работать независимо от версии Django:

 (queryset
    .values('owner', owner_name=F('owner__name'))
    .annotate(owner_count=Count('owner'))
    .annotate(views = Case(
        When(owner_count__gt=1,
             then=Sum(F('views') / date_delta)),
        default=Sum('views'),
        output_field=IntegerField()
    ))
)
 

Небольшим изменением может быть использование подзапроса. Подзапрос Raydel, который вычисляет Trenddata количество для каждого Owner , работает в принципе, но будет непомерно медленным, поскольку он выполняет агрегацию для каждой отдельной строки Trenddata (а не только для уникальных Owner s).

Другой подзапрос обеспечивает более быстрый способ получения того же результата. Он выполняет тяжелую работу по подсчету Owner s Trenddata только один раз, а затем проверяет для каждого Trenddata объекта, есть ли его владелец в списке. Я бы подумал, что это все равно должно быть медленнее, чем мой первый запрос, но, как ни странно, в моих коротких тестах он оказался на одном уровне (около 3 м строк).

 (queryset
    .values('owner', owner_name=F('owner__name'))
    .annotate(multi=Case(
        When(owner__in=Subquery(TrendData.objects
                                    .values('owner')
                                    .annotate(cnt=Count('owner'))
                                    .filter(cnt__gt=0)
                                    .values('owner')), 
             then=1),
        default=0,
        output_field=IntegerField())
    ) 
    .annotate(views = Case(
        When(multi=1,
             then=Sum(F('views') / date_delta)),
        default=Sum('views'),
        output_field=IntegerField())
    )
)
 

Вы можете объединить две аннотации в одну, но если вы повторно multi используете еще несколько аннотаций, а не только одну, как в моем примере, разделение двух позволяет избежать повторения подзапроса для каждой аннотации.

Комментарии:

1. У меня та же ошибка, о которой я упоминал в комментарии @Raydel выше: (

2. хм, я попробовал еще раз, это не так: (но я думаю, я должен упомянуть, что мой django равен 1.11 :/

3. Я думаю, что ошибка была с моей стороны, а не из-за версий Django. Если хотите, попробуйте выполнить два вышеуказанных запроса.

Ответ №2:

Во-первых, я думаю, что это неправильно owner_name=F('owner_id__name' , это должно быть owner_name=F('owner__name' .

Если я понял, вы хотите аннотировать набор запросов TrendData количеством экземпляров TrendData, у которых есть владелец.

Вы можете использовать подзапрос для достижения этого:

 owner_td_count = Owner.objects.annotate(
    td_count=Count('trenddata_set')
).filter(
    id=OuterRef('owner_id')
).values('td_count')[:1]
 

Затем сначала аннотируйте, подсчитывая вхождения owner_id:

 queryset.annotate(
    owner_name=F('owner__name'),
    owner_id_count=Subquery(owner_td_count)   # How many DataTrend's have the owner with id=owner_id
    ).values('owner_name', 'owner_id').annotate(
        # ...
    )
)
 

Тогда вы могли бы в вашем случае / при построении:

 Case(
    When(
        owner_id_count=1, then=F('views'), 
        default=F('views') / date_delta)),
        output_field=IntegerField() 
    )
)
 

Комментарии:

1. похоже, что это так, однако я получаю сообщение об ошибке: ( django.core.exceptions.FieldError: Cannot compute Sum('<Case: CASE WHEN <Q: (AND: ('owner_id_count__gt', 1))> THEN <CombinedExpression: F(views) / Value(2)>, ELSE Value(None)>'): '<Case: CASE WHEN <Q: (AND: ('owner_id_count__gt', 1))> THEN <CombinedExpression: F(views) / Value(2)>, ELSE Value(None)>' is an aggregate

2. Возможно, проблема заключалась в том, что агрегаты не поддерживают поиск внутри When выражений, я редактирую ответ, чтобы избежать использования __gt поиска в owner_id_count агрегате внутри выражения When .

3.Вам нужно выбрать некоторые (неуникальные) values перед подсчетом, иначе счетчик всегда будет 1 .

4. @EndreBoth Нет, вы этого не делаете. owner_id может быть результатом наличия поля owner , которое является внешним ключом к другой модели. И внешний ключ (может быть, но …) не гарантируется, что он будет уникальным для всей таблицы.

5. Да, тогда обновленный ответ — это решение, попробуйте. Пожалуйста, примите во внимание, что я это не тестировал, это попытка указать вам правильное направление.