Django виділяє лише рядки з повторюваними значеннями полів


96

припустимо, у нас є модель у 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?

Відповіді:


193

Спробуйте:

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

Це так близько, як ви можете дістатися з Джанго. Проблема полягає в тому, що це поверне лише 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)?
драгун

Оригінальний запит даєCannot resolve keyword 'id_count' into field
драгун

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)що дозволить це все виконувати в одному запиті.
Пайпер Мерріам

43

Це було відхилено як редагування. Отже, це краща відповідь

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

Це поверне a 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 - ні, але Кріс Пратт відповів альтернативою у своїй відповіді.
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')
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.