Django仅选择具有重复字段值的行

140

假设我们在Django中定义了一个模型,如下所示:

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

名称字段不是唯一的,因此可能有重复的值。我需要完成以下任务: 从模型中选择所有具有名称字段的至少一个重复值的行。

我知道如何使用普通 SQL 完成它(也许不是最好的解决方案):

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

那么,使用Django ORM选择它是可能的吗?还是使用更好的SQL解决方案?

6个回答

283

尝试:

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

这是在Django中能够得到的最接近的结果。问题是这将返回一个ValuesQuerySet,只包含namecount。但是,您可以通过将其反馈到另一个查询中来使用它构建常规的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])

9
也许你的意思是"Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)"? - dragoon
3
谢谢更新的答案,我认为我会坚持使用这个解决方案。你甚至可以使用values_list('name',flat=True)来完成它,而不需要列表推导式。 - dragoon
FieldError: Join on field 'id' not permitted. Did you misspell 'count' for the lookup type? - wRAR
3
Django以前存在一个错误(可能已在最近的版本中修复),即如果您不为“Count”注释指定要保存的字段名称,则默认为“[field]__count”。但是,双下划线语法也是Django解释您想要执行连接的方式。因此,当您尝试在其上进行过滤时,Django认为您正在尝试使用“count”进行连接,而这显然不存在。解决方法是为您的注释结果指定一个名称,即annotate(mycount=Count('id')),然后在mycount上进行过滤。 - Chris Pratt
1
如果在您的注释调用之后添加另一个 values('name') 调用,则可以删除列表推导式并说 Literal.objects.filter(name__in=dupes),这将允许所有内容在单个查询中执行。 - Piper Merriam
显示剩余7条评论

64

这篇文章被拒绝作为编辑。所以,这里它以一种更好的回答的形式呈现。

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

这将返回一个带有所有重复名称的 ValuesQuerySet。但是,您可以通过将其反馈到另一个查询中来构建一个常规的 QuerySet。Django ORM 足够智能以将它们合并为单个查询:

Literal.objects.filter(name__in=dups)

在annotate调用之后再次调用.values('name')看起来有点奇怪。如果没有这个语句,子查询就会失败。额外的values语句会欺骗ORM只选择子查询中的名称列。

很棒的技巧,不幸的是,这只适用于使用一个值的情况(例如,如果同时使用“name”和“phone”,最后一部分将无法工作)。 - gdvalderrama
2
.order_by()有什么作用? - stefanfoulis
4
它清除了任何现有的排序。如果您有模型集排序,则这将成为 SQL 中 GROUP BY 子句的一部分,并且会破坏它。当使用子查询进行游戏时发现了这一点(在其中通过 .values() 进行非常类似的分组)。 - Oli

12

尝试使用聚合

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)

好的,这给出了正确的名称列表,但是是否可以同时选择ID和其他字段呢? - dragoon
@dragoon - 不过克里斯·普拉特在他的回答中提到了另一种选择。 - JamesO

7
如果您使用的是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

1
我尝试了这个,但是 Python 代码报错:FieldError: Expression contains mixed types: ArrayField, IntegerField. You must set output_field.。然而,SQL 查询像预期的那样工作(Django 3.2)。 - oglop
非常好用(Django 2.2)。此外,您不需要使用array_length注释,而是可以通过ids__len进行过滤-https://docs.djangoproject.com/en/dev/ref/contrib/postgres/fields/#len - a regular fellow

2

好的,由于某些原因,以上方法都没有生效,它总是返回<MultilingualQuerySet []>。我使用以下更易理解但不太优雅的解决方案:

dupes = []
uniques = []

dupes_query = MyModel.objects.values_list('field', flat=True)

for dupe in set(dupes_query):
    if not dupe in uniques:
        uniques.append(dupe)
    else:
        dupes.append(dupe)

print(set(dupes))

0
如果你只想要结果中的名称列表而不是对象,你可以使用以下查询。
repeated_names = Literal.objects.values('name').annotate(Count('id')).order_by().filter(id__count__gt=1).values_list('name', flat='true')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接