Почему фильтры влияют на результат StringAgg?

Я использую StringAgg и order следующим образом:

    # Get order column & annotate with list of credits
    if request.POST.get('order[0][name]'):
        order = request.POST['order[0][name]']
        if order == 'credits_primary':
            releases = releases.annotate(credits_primary=StringAgg(
                'credits__entity__name',
                delimiter=', ',
                filter=Q(credits__type='primary'),
                ordering='credits__id'
            ))
        elif order == 'credits_secondary':
            releases = releases.annotate(credits_secondary=StringAgg(
                'credits__entity__name',
                delimiter=', ',
                filter=Q(credits__type='secondary'),
                ordering='credits__id'
            ))
    else:
        order = 'title'
    
    # Order releases
    if request.POST.get('order[0][dir]') == 'desc':
        releases = releases.order_by(F(order).desc(nulls_last=True), 'title')
    else:
        releases = releases.order_by(F(order).asc(nulls_last=True), 'title')
    
    for release in releases:
        try: print(release.credits_primary)
        except: pass
        try: print(release.credits_secondary)
        except: pass

Это само по себе работает именно так, как ожидалось: упорядочивание соответствует моим ожиданиям, и print возвращает ожидаемые значения.

Однако, когда я применяю

перед этим filter, он начинает вести себя странно. А именно: иногда все в порядке и все работает, как ожидалось, иногда каждый credits__entity__name повторяется случайное число раз, иногда аннотация просто возвращает None, хотя значения есть. Я не могу понять, в чем здесь закономерность.

Ниже приведены фильтры, которые я применяю, обратите внимание, что exclude, насколько я могу судить, не вызывает этой проблемы:

    if request.POST.get('entity'):
        releases = Release.objects.filter(credits__entity=request.POST['entity'])
    else:
        releases = Release.objects.all()
        
    records_total = releases.count()
        
    # Filter by type
    if request.POST.get('type'):
        query = Q()
        for type in loads(request.POST['type']):
            if type in Release.TYPES_P_TO_S:
                query.add(Q(type=Release.TYPES_P_TO_S[type]), Q.OR)
        releases = releases.filter(query)
        
    # Filter by platform
    if request.POST.get('platform'): releases = releases.filter(platforms__in=loads(request.POST['platform']))
        
    # Filter by format
    if request.POST.get('format'): releases = releases.filter(formats__in=loads(request.POST['format']))
        
    # Filter by From/Until year
    if request.POST.get('from'): releases = releases.filter(date__year__gte=loads(request.POST['from'])[0])
    if request.POST.get('until'): releases = releases.filter(date__year__lte=loads(request.POST['until'])[0])
        
    # Exclude parent types
    if request.POST.get('exclude'):
        query = Q()
        for type in loads(request.POST['exclude']):
            if type in Release.PARENT_TYPES_P_TO_S:
                query.add(Q(parent_type=Release.PARENT_TYPES_P_TO_S[type]), Q.OR)
        releases = releases.exclude(query)
        
    # Filter by Search
    if request.POST.get('search[value]'):
        query = Q()
        search = request.POST['search[value]']
        query.add(Q(title__icontains=search), Q.OR)
        query.add(Q(tags__tag__name__icontains=search), Q.OR)
        query.add(Q(credits__entity__name__icontains=search), Q.OR)
        releases = releases.filter(query)
        
    # Make sure items aren't repeated
    releases = releases.distinct()

Не уверен, что я понимаю ваш вариант использования, но если проблема в том, что вы получаете дубликаты в своей агрегации, возможно, вы можете попробовать использовать distinct в своих агрегациях строк:

https://docs.djangoproject.com/fr/5.1/ref/contrib/postgres/aggregates/#stringagg

Ниже приведен применяемый мной фильтр, который влияет на агрегацию (применяется перед StringAgg):

Фильтр рассматривает (другие) отношения "один ко многим" или "многие ко многим". Это означает, что результатом будет LEFT OUTER JOIN, который начнет действовать как множитель. То же самое работает с другими агрегатами, такими как SUM и COUNT.

Действительно, если вы просто агрегируете данные, запрос выглядит следующим образом:

SELECT string_agg(CASE
                      WHEN c.type = 'primary' THEN e.name
                      ELSE NULL
                  END
                  ORDER BY e.id)
FROM releases AS r
LEFT OUTER JOIN credits AS c ON c.release_id = r.id
LEFT OUTER JOIN entity AS e ON e.credit_id = e.id

и это нормально, потому что мы хотим агрегировать данные по сущностям, поэтому в каждом выпуске мы получаем все сущности.

Но теперь, если мы, например, отфильтруем по тегам, это будет выглядеть так:

SELECT string_agg(CASE
                      WHEN c.type = 'primary' THEN e.name
                      ELSE NULL
                  END
                  ORDER BY e.id)
FROM releases AS r
LEFT OUTER JOIN credits AS c ON c.release_id = r.id
LEFT OUTER JOIN entity AS e ON e.credit_id = e.id
LEFT OUTER JOIN release_tag rt1 ON rt1.release_id = r.id
LEFT OUTER JOIN tag t1 ON rt1.tag_id = t1.id
LEFT OUTER JOIN release_tag rt2 ON rt2.release_id = r.id
LEFT OUTER JOIN tag t1 ON rt2.tag_id = t2.id
WHERE t1.name = 'tag1'
  OR t2.name = 'tag2'

Это означает, что имена сущностей для выпуска, в котором совпадает один из двух тегов, будут повторены один раз, но в случае совпадения обоих тегов они будут включены дважды.

Q(credits__entity__name__icontains) делает ситуацию еще хуже: поскольку это приведет к объединению в таблице c, и, следовательно, объекты из credits, которые имеют два или более совпадений, будут включены так много раз, тогда как объекты выпуска, которые не соответствуют заданному кредиту, больше не будут включены.

Наименьшим общим признаком всех этих явлений, вероятно, является то, что вы должны быть очень осторожны при составлении агрегатов при создании JOIN, особенно если несколько таблиц, в которых выполняются объединения. в этом замешаны наши родственники.

Что вы, вероятно, можете сделать, так это создать Subquery выражение [Django-doc] на который, таким образом, не влияют "внешние" таблицы и соответствующие JOIN.

Вернуться на верх