根据行顺序在多个字段中搜索

Searching in multiple fields respecting the row order

我有如下模型:

class Foo(models.Model):
    fruit = models.CharField(max_length=10)
    stuff = models.CharField(max_length=10)
    color = models.CharField(max_length=10)
    owner = models.CharField(max_length=20)
    exists = models.BooleanField()
    class Meta:
        unique_together = (('fruit', 'stuff', 'color'), )

其中填充了一些数据:

fruit  stuff  color   owner  exists
Apple  Table   Blue     abc    True
 Pear   Book    Red     xyz   False
 Pear  Phone  Green     xyz   False
Apple  Phone   Blue     abc    True
 Pear  Table  Green     abc    True

我需要merge/join这个集合(不是查询集):

[('Apple', 'Table', 'Blue'), ('Pear', 'Phone', 'Green')]

所以当我用这个元组列表搜索这个模型时,基本上第 0 行和第 2 行应该 return。

目前我的解决方法是将 Foo.objects.all() 读入 DataFrame 并与元组列表合并并获取要传递给 Foo.objects.filter() 的 ID。我还尝试遍历列表并在每个元组上调用 Foo.object.get() 但它非常慢。名单相当大。

当我尝试按照当前答案的建议链接 Q 时,它抛出了操作错误(太多 SQL 变量)。

我的主要目标如下:

从模型中可以看出,这三个字段共同构成了我的主键。 table 包含大约 15k 个条目。当我从另一个来源获取数据时,我需要相应地检查数据是否已经在我的 table 和 create/update/delete 中(新数据可能包含多达 15k 个条目)。有没有一种干净有效的方法来检查这些记录是否已经在我的table中?

注意:元组列表不必是那种形状。我可以修改它,把它变成另一个数据结构或者转置它。

您对 Q 所做的是 AND 在所有 where in 语句之间

你想要实现的是 OR 所有 Q 元组属性设置如下

Foo.objects.filter(Q(fruit='Apple',stuff='Pear',color='Blue)|Q...

要执行此程序化操作,您可以执行以下操作:

tuple = [('Apple', 'Table', 'Blue'), ('Pear', 'Phone', 'Green')]

query = reduce(lambda q,value: q|Q(fruit=value[0], stuff=value[1], color=value[2]), tuple, Q())  

Foo.objects.filter(query)

这是正确的查询:

q = Foo.objects.filter(
    Q(fruit='Apple', stuff='Table', color='Blue') |
    Q(fruit='Pear', stuff='Phone', color='Green')
)

此查询也可以使用(如果您不喜欢 Q):

q = Foo.objects.filter(
    fruit='Apple', stuff='Table', color='Blue'
) | Foo.objects.filter(
    fruit='Pear', stuff='Phone', color='Green'
)

你有 ('fruit', 'stuff', 'color') 个字段在一起是唯一的

因此,如果您的搜索元组是 ('Apple', 'Table', 'Blue') 并且我们将其连接起来,那么它也将是一个唯一的字符串

f = [('Apple', 'Table', 'Blue'), ('Pear', 'Phone', 'Green')]
c = [''.join(w) for w in f]
# Output: ['AppleTableBlue', 'PearPhoneGreen']

所以我们可以在 annotations and make use of Concat.

上过滤查询集
Foo.objects.annotate(u_key=Concat('fruit', 'stuff', 'color', output_field=CharField())).filter(u_key__in=c)
# Output: <QuerySet [<Foo: #0row >, <Foo: #2row>]>

This will work for tuple and list

转置大小写

案例 1:

如果输入是 2 元组的列表:

[('Apple', 'Table', 'Blue'), ('Pear', 'Phone', 'Green')]

转置输入后将是:

transpose_input = [('Apple', 'Pear'), ('Table', 'Phone'), ('Blue', 'Green')]

We can easily identify by counting each_tuple_size and input_list_size that the input is transposed. so we can use zip to transpose it again and the above solution will work as expected.

if each_tuple_size == 2 and input_list_size == 3:
    transpose_again = list(zip(*transpose_input))
    #  use *transpose_again* variable further

案例 2:

如果输入是 3 元组的列表:

[('Apple', 'Table', 'Blue'), ('Pear', 'Phone', 'Green'), ('Pear', 'Book', 'Red')]

转置后输入将是:

transpose_input = [('Apple', 'Pear', 'Pear'), ('Table', 'Phone', 'Book'), ('Blue', 'Green', 'Red')]

So it is impossible to identify that the input is transposed for every n*n matrix and above solution will Fail

如果您知道这些字段构成了您的自然键并且您必须对它们进行大量查询,请将此自然键添加为适当的字段并采取措施对其进行维护:

class FooQuerySet(models.QuerySet):
    def bulk_create(self, objs, batch_size=None):
        objs = list(objs)
        for obj in objs:
            obj.natural_key = Foo.get_natural_key(obj.fruit, obj.stuff, obj.color)
        return super(FooQuerySet, self).bulk_create(objs, batch_size=batch_size)

    # you might override update(...) with proper F and Value expressions, 
    # but I assume the natural key does not change

class FooManager(models.Manager):
    def get_queryset(self):
        return FooQuerySet(self.model, using=self._db)

class Foo(models.Model):
    NK_SEP = '|||'  # sth unlikely to occur in the other fields

    fruit = models.CharField(max_length=10)
    stuff = models.CharField(max_length=10)
    color = models.CharField(max_length=10)
    natural_key = models.CharField(max_length=40, unique=True, db_index=True)

    @staticmethod
    def get_natural_key(*args):
        return Foo.NK_SEP.join(args) 

    def save(self, *args, **kwargs):
        self.natural_key = Foo.get_natural_key(self.fruit, self.stuff, self.color)
        Super(Foo, self).save(*args, **kwargs)

    objects = FooManager()

    class Meta:
        unique_together = (('fruit', 'stuff', 'color'), )

现在可以查询:

from itertools import starmap

lst = [('Apple', 'Table', 'Blue'), ('Pear', 'Phone', 'Green')]
existing_foos = Foo.objects.filter(natural_key__in=list(starmap(Foo.get_natural_key, lst)))

并批量创建:

Foo.objects.bulk_create(
    [
        Foo(fruit=x[0], stuff=x[1], color=x[2]) 
        for x in lst 
        if x not in set(existing_foos.values_list('fruit', 'stuff', 'color'))
    ]
)

这个问题很可能是X/Y问题的体现。你问的不是你的问题 X,而是你想出的解决方案 Y。

你为什么要保留计数器字段?我的意思是,为什么不删除计数字段并使用以下查询:

Foo.objects.order_by('fruit', 'stuff', 'color')\
           .values('fruit', 'stuff', 'color')\
           .annotate(count=Count('*'))

或者保留它但使用计数总和代替:

Foo.objects.order_by('fruit', 'stuff', 'color')\
           .values('fruit', 'stuff', 'color')\
           .annotate(total=Sum('count'))

如果您删除 unique_together 约束,您只需在数据库中插入新条目即可合并数据集:

for fruit, stuff, color in collection:
    Foo.objects.update_or_create(fruit=fruit, stuff=stuff, color=color)

或者假设集合是键和计数的字典:

for fruit, stuff, color in collection:
    Foo.objects.update_or_create(
         fruit=fruit, 
         stuff=stuff, 
         color=color,
         count=F('count') + collection[(fruit, stuff, color)],
    )

请不要回答 "it is for performance reasons",除非您已经分析了这两种方法 - 在我看来,保持分数是数据库的工作。如果您尝试并真正发现性能问题,那么有能力的 DBA 将提出解决方案(在极少数情况下,它可能涉及通过使用数据库触发器来保持辅助 table 计数)。

我的观点是,保留一个可以由数据库计算的值是一个有问题的设计。您必须有一个很好的理由,并且您必须首先分析 'let the database calculate it' 方法 - 否则您可能会因为虚构的性能原因而使您的设计复杂化。

无论如何,我想不出有什么策略可以使它比 O(n) 更好 - n 是您要合并的数据集中的条目数。

那么我可能猜错了你原来的问题所以如果是这样请告诉我们。