Django выбирает только строки с повторяющимися значениями полей

99

предположим, что у нас есть модель в django, определенная следующим образом:

class Literal:
    name = models.CharField(...)
    ...

Поле имени не уникально и, следовательно, может иметь повторяющиеся значения. Мне нужно выполнить следующую задачу: Выбрать все строки из модели , которые имеют по крайней мере один повторяющееся значение в nameполе.

Я знаю, как это сделать с помощью простого SQL (может быть, не лучшее решение):

select * from literal where name IN (
    select name from literal group by name having count((name)) > 1
);

Итак, можно ли выбрать это с помощью django ORM? Или лучшее решение SQL?

драгуна
источник

Ответы:

199

Пытаться:

from django.db.models import Count
Literal.objects.values('name')
               .annotate(Count('id')) 
               .order_by()
               .filter(id__count__gt=1)

Это максимально приближено к Django. Проблема в том, что это вернет ValuesQuerySetтолько nameи count. Однако затем вы можете использовать это для создания регулярного QuerySetзапроса, передав его обратно в другой запрос:

dupes = Literal.objects.values('name')
                       .annotate(Count('id'))
                       .order_by()
                       .filter(id__count__gt=1)
Literal.objects.filter(name__in=[item['name'] for item in dupes])
Крис Прэтт
источник
5
Вы, наверное, имели в виду Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
dragoon
Исходный запрос даетCannot resolve keyword 'id_count' into field
dragoon
2
Спасибо за обновленный ответ, думаю, я буду придерживаться этого решения, вы даже можете сделать это без понимания списка, используяvalues_list('name', flat=True)
dragoon
1
В Django ранее была ошибка (могла быть исправлена ​​в последних версиях), где, если вы не укажете имя поля для Countаннотации, которая будет сохранена как, по умолчанию будет использоваться [field]__count. Однако этот синтаксис с двойным подчеркиванием также интерпретирует Django, что вы хотите выполнить соединение. Итак, по сути, когда вы пытаетесь отфильтровать это, Django думает, что вы пытаетесь выполнить соединение, countкоторого явно не существует. Исправление состоит в том, чтобы указать имя для результата аннотации, т.е. вместо annotate(mycount=Count('id'))этого включить фильтрацию mycount.
Крис Пратт
1
если вы добавите еще один вызов values('name')после вашего вызова для аннотирования, вы можете удалить понимание списка и сказать, Literal.objects.filter(name__in=dupes)что позволит всем этим выполняться в одном запросе.
Пайпер Мерриам,
45

Это было отклонено как правка. Итак, вот как лучший ответ

dups = (
    Literal.objects.values('name')
    .annotate(count=Count('id'))
    .values('name')
    .order_by()
    .filter(count__gt=1)
)

Это вернет ValuesQuerySetсо всеми повторяющимися именами. Однако затем вы можете использовать это для создания регулярного QuerySetзапроса, передав его обратно в другой запрос. ORM django достаточно умен, чтобы объединить их в один запрос:

Literal.objects.filter(name__in=dups)

Дополнительный вызов .values('name')после вызова аннотации выглядит немного странно. Без этого подзапрос завершится ошибкой. Дополнительные значения заставляют ORM выбирать только столбец имени для подзапроса.

Пайпер Мерриам
источник
Хороший трюк, к сожалению, это сработает, только если используется только одно значение (например, если используются и «имя», и «телефон», последняя часть работать не будет).
guival
1
Для чего .order_by()?
stefanfoulis
4
@stefanfoulis Удаляет все существующие заказы. Если у вас есть порядок набора моделей, это становится частью предложения SQL GROUP BY, и это ломает вещи. Обнаружил это, играя с подзапросом (в котором вы выполняете очень похожую группировку через .values())
Оли
10

попробуйте использовать агрегацию

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)
ДжеймсО
источник
Хорошо, это дает соответствующий список имен, но можно ли одновременно выбрать идентификаторы и другие поля?
dragoon
@dragoon - нет, но Крис Пратт осветил альтернативу в своем ответе.
JamesO
5

Если вы используете PostgreSQL, вы можете сделать что-то вроде этого:

from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import Func, Value

duplicate_ids = (Literal.objects.values('name')
                 .annotate(ids=ArrayAgg('id'))
                 .annotate(c=Func('ids', Value(1), function='array_length'))
                 .filter(c__gt=1)
                 .annotate(ids=Func('ids', function='unnest'))
                 .values_list('ids', flat=True))

В результате получается довольно простой SQL-запрос:

SELECT unnest(ARRAY_AGG("app_literal"."id")) AS "ids"
FROM "app_literal"
GROUP BY "app_literal"."name"
HAVING array_length(ARRAY_AGG("app_literal"."id"), 1) > 1
Евгений Пахомов
источник
0

Если вы хотите получить только список имен, но не объекты, вы можете использовать следующий запрос

repeated_names = Literal.objects.values('name').annotate(Count('id')).order_by().filter(id__count__gt=1).values_list('name', flat='true')
user2959723
источник