【问题标题】:Searching in multiple fields respecting the row order根据行顺序在多个字段中搜索
【发布时间】:2018-05-08 21:47:55
【问题描述】:

我有一个类似下面的模型:

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

我需要将它与集合(不是查询集)合并/加入:

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

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

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

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

我的主要目标如下:

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

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

【问题讨论】:

  • 您使用的是哪个数据库?
  • @PaoloMelchiorre 我的试验是在 sqlite 上,但我可以切换到 postgresql。
  • 你试过Foo.objects.filter(fruit="Apple").filter(stuff="Table").filter(color="Blue")。这基本上应该为第一个查询命中数据库一次,然后在较小的搜索空间中在本地执行剩余的两个查询。

标签: python django django-queryset django-orm


【解决方案1】:

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

你想要实现的是 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)

【讨论】:

  • 我没有使用reduce,我用this尝试过,但考虑到它们会生成相同数量的变量,这似乎也不是一个可行的替代方案。
  • @ayhan 这个解决方案最终与 aliva 解决方案具有相同的结果,或者您链接的结果只是以不同的形式编写的
【解决方案2】:

这是正确的查询:

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'
)

【讨论】:

  • 它抛出“OperationalError: too many SQL variables”。将 Q 链接在一起真的比调用 .get()s 有什么优势吗?
  • @ayhan 您调用的每个 .get() 都会对数据库进行一次查询,因此您有 n 个查询,随着查询的增长,预计大链上会出现以下错误行为,因此将其划分为几个查询会是性能明智的最佳选择
【解决方案3】:

你有 ('fruit', 'stuff', 'color') 字段一起唯一

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

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

所以我们可以过滤 annotations 上的查询集并使用 Concat。 p>

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

这适用于 tuplelist

转置大小写

案例一:

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

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

转置后输入将是:

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

我们可以通过计算 each_tuple_size 和 input_list_size 轻松识别 输入被转置。所以我们可以使用 zip 来 再次转置它,上述解决方案将按预期工作

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')]

因此不可能确定输入是针对每个 n*n 矩阵及以上解决方案将失败

【讨论】:

    【解决方案4】:

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

    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'))
        ]
    )
    

    【讨论】:

      【解决方案5】:

      这个问题很可能是 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)],
          )
      

      请不要回答“这是出于性能原因”,除非您已经分析了这两种方法 - 在我不那么谦虚的观点中,保持分数是数据库的工作。如果您尝试并确实发现了性能问题,那么称职的 DBA 会提出解决方案(在极少数情况下,它可能涉及通过使用数据库触发器来保存一个带有计数的辅助表)。

      我的观点是,保留一个可以由数据库计算的值是一个有问题的设计。您必须有充分的理由,并且您必须首先分析“让数据库计算它”的方法 - 否则您可能会因为想象中的性能原因而使您的设计复杂化。

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

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

      【讨论】:

      • count 字段与表格中出现的水果、东西、颜色组合的次数无关,它只是我从其他来源获得的该记录的一个属性。
      • 哦,我想如果没有它,这个例子就不会那么混乱了。 update_or_create 的建议是否仍然适用?
      • 让我把它改成别的。馆藏相当大。如果我使用update_or_create,它将为循环的每次迭代创建一个新查询并产生大量开销。例如,如果我在循环中使用bulk_create 而不是create,则时间会从几分钟下降到几毫秒。尝试使用单个查询也会失败(请参阅this comment)。这就是为什么我专注于确定要更改的行,希望我可以根据需要更新的百分比制定更好的策略。
      • 抱歉,除非您使用临时表和一些原始 SQL,否则我认为您不会比 O(n) 更好。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-04-16
      • 1970-01-01
      • 1970-01-01
      • 2020-07-11
      • 1970-01-01
      • 1970-01-01
      • 2017-02-03
      相关资源
      最近更新 更多