【问题标题】:How to pull a random record using Django's ORM?如何使用 Django 的 ORM 提取随机记录?
【发布时间】:2010-11-01 01:09:25
【问题描述】:

我有一个模型来代表我在我的网站上展示的画作。在主网页上,我想展示其中的一些:最新的、大部分时间未访问的、最受欢迎的和随机的。

我正在使用 Django 1.0.2。

虽然前 3 个使用 django 模型很容易提取,但最后一个(随机)给我带来了一些麻烦。在我看来,我可以将其编码为如下所示:

number_of_records = models.Painting.objects.count()
random_index = int(random.random()*number_of_records)+1
random_paint = models.Painting.get(pk = random_index)

它看起来不像我想要的东西——这完全是数据库抽象的一部分,应该在模型中。另外,在这里我需要处理已删除的记录(那么所有记录的数量不会涵盖我所有可能的键值)以及可能还有很多其他事情。

我可以如何做到这一点,最好是在模型抽象内部的任何其他选项?

【问题讨论】:

  • 在我看来,如何显示内容以及显示哪些内容是“视图”级别或业务逻辑的一部分,应该进入 MVC 的“控制器”级别。
  • 在 Django 中,控制器是视图。 docs.djangoproject.com/en/dev/faq/general/…
  • 应该有一个内置函数——不使用order_by('?')

标签: python django django-models


【解决方案1】:

简单使用:

MyModel.objects.order_by('?').first()

它记录在QuerySet API

【讨论】:

  • 请注意,这种方法可能非常慢,如文档所示:)
  • “可能昂贵且缓慢,具体取决于您使用的数据库后端。” - 有关于不同数据库后端的经验吗? (sqlite/mysql/postgres)?
  • 我没有测试过,所以这纯粹是猜测:为什么它比检索所有项目并在 Python 中执行随机化要慢?
  • 我读到它在 mysql 中很慢,因为 mysql 的随机排序非常低效。
  • 为什么不直接random.choice(Model.objects.all())
【解决方案2】:

使用order_by('?') 将在生产的第二天杀死数据库服务器。更好的方法类似于Getting a random row from a relational database 中描述的方法。

from django.db.models.aggregates import Count
from random import randint

class PaintingManager(models.Manager):
    def random(self):
        count = self.aggregate(count=Count('id'))['count']
        random_index = randint(0, count - 1)
        return self.all()[random_index]

【讨论】:

  • model.objects.aggregate(count=Count('id'))['count']model.objects.all().count() 有什么好处
  • 虽然比公认的答案要好得多,但请注意,这种方法会进行两个 SQL 查询。如果计数在两者之间发生变化,则可能会出现越界错误。
  • 也许注释 random(self) 应该用“@transaction.atomic”注释以避免更改计数问题? docs.djangoproject.com/ja/1.9/topics/db/transactions
  • 这是一个错误的解决方案。如果您的 id 不是从 0 开始,它将不起作用。并且当 id 不连续时也是如此。比如说,第一条记录从 500 开始,最后一条记录是 599(假设连续)。那么计数将是 54950。肯定 list[54950] 不存在,因为您的查询的长度是 100。它会抛出索引超出范围异常。我不知道为什么有这么多人赞成这个,这被标记为接受的答案。
  • @sajid:你为什么要问我?很容易看到我对这个问题的贡献的总和:编辑链接以指向腐烂后的档案。我什至没有对任何答案投票。但我确实觉得这个答案和你声称要好得多的答案都使用.all()[randint(0, count - 1)] 实际上很有趣。也许您应该专注于找出答案的哪一部分是错误的或薄弱的,而不是为我们重新定义“一错再错”并对愚蠢的选民大喊大叫。 (可能是没有使用.objects?)
【解决方案3】:

如果您使用 MySQL,即使对于中型表,order_by('?')[:N] 的解决方案也非常慢(不了解其他数据库)。

order_by('?')[:N] 将被翻译成SELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT N 查询。

表示对表中的每一行执行RAND()函数,然后根据该函数的值对整个表进行排序,然后返回前N条记录。如果你的桌子很小,这很好。但在大多数情况下,这是一个非常慢的查询。

我写了一个简单的函数,即使 id 有漏洞(某些行被删除)也能工作:

def get_random_item(model, max_id=None):
    if max_id is None:
        max_id = model.objects.aggregate(Max('id')).values()[0]
    min_id = math.ceil(max_id*random.random())
    return model.objects.filter(id__gte=min_id)[0]

几乎在所有情况下它都比 order_by('?') 快。

【讨论】:

  • 另外,可悲的是,它远非随机。如果您有一个 ID 为 1 的记录和另一个 ID 为 100 的记录,那么它将在 99% 的情况下返回第二个记录。
【解决方案4】:

这是一个简单的解决方案:

from random import randint

count = Model.objects.count()
random_object = Model.objects.all()[randint(0, count - 1)] #single random object

【讨论】:

    【解决方案5】:

    你可以在你的模型上创建一个manager 来做这种事情。首先了解什么是管理器,Painting.objects 方法是一个包含all()filter()get() 等的管理器。创建自己的管理器允许您预先过滤结果并拥有所有这些相同的方法,以及您自己的自定义方法,处理结果。

    编辑:我修改了我的代码以反映order_by['?'] 方法。请注意,管理器返回无限数量的随机模型。因此,我包含了一些使用代码来展示如何获得一个模型。

    from django.db import models
    
    class RandomManager(models.Manager):
        def get_query_set(self):
            return super(RandomManager, self).get_query_set().order_by('?')
    
    class Painting(models.Model):
        title = models.CharField(max_length=100)
        author = models.CharField(max_length=50)
    
        objects = models.Manager() # The default manager.
        randoms = RandomManager() # The random-specific manager.
    

    用法

    random_painting = Painting.randoms.all()[0]
    

    最后,您的模型中可以有多个经理,因此请随意创建LeastViewsManager()MostPopularManager()

    【讨论】:

    • 只有在您的 pk 是连续的情况下使用 get() 才有效,即您永远不会删除任何项目。否则你很可能会尝试得到一个不存在的pk。使用 .all()[random_index] 不会遇到这个问题,效率也不会降低。
    • 我明白这就是为什么我的示例只是简单地将问题的代码复制给经理。仍然由 OP 来进行边界检查。
    • 而不是使用 .get(id=random_index) 使用 .filter(id__gte=random_index)[0:1] 会更好吗?首先,它有助于解决非连续pks的问题。其次,get_query_set 应该返回...一个 QuerySet。在你的例子中,它没有。
    • 我不会为了容纳一种方法而创建新的管理器。我会将“get_random”添加到默认管理器中,这样您就不必在每次需要随机图像时都经过 all()[0] 循环。此外,如果作者是用户模型的外键,您可以说 user.painting_set.get_random()。
    • 当我想要一个全面的操作时,我通常会创建一个新经理,比如获取随机记录列表。如果我正在使用我已经拥有的记录执行更具体的任务,我会在默认管理器上创建一个方法。
    【解决方案6】:

    其他答案可能很慢(使用order_by('?'))或使用多个 SQL 查询。这是一个没有排序的示例解决方案,只有一个查询(假设 Postgres):

    random_instance_or_none = Model.objects.raw('''
        select * from {0} limit 1
        offset floor(random() * (select count(*) from {0}))
    '''.format(Model._meta.db_table)).first()
    

    请注意,如果表为空,这将引发索引错误。为自己编写一个与模型无关的辅助函数来检查它。

    【讨论】:

    • 一个很好的概念证明,但这也是数据库内部的两个查询,您保存的是到数据库的一次往返。您必须多次执行此操作才能使编写和维护原始查询值得。如果您想防止空表,您不妨提前运行count() 并省去原始查询。
    【解决方案7】:

    我只是一个简单的想法:

    def _get_random_service(self, professional):
        services = Service.objects.filter(professional=professional)
        i = randint(0, services.count()-1)
        return services[i]
    

    【讨论】:

      【解决方案8】:

      DB 中的随机化在 python 中感觉很糟糕但更好。但同时,为了忽略大部分结果(尤其是在生产环境中),将所有数据从 DB 带到 python 内存中并不是一个好主意。我们可能还需要某种过滤。

      1. 所以基本上我们在 DB 有数据,
      2. 我们想用python的rand函数
      3. 随后从 DB 中调出所需的全部数据。

      基本上使用 2 个查询比在 DB CPU 中随机选择(在 DB 中计算)或加载整个数据(大量网络利用率)要便宜得多。解释的解决方案必须具有可扩展性,试图在这里进行计划将不适用于生产环境,尤其是带有过滤器、软/硬删除,甚至带有 is_public 标志的生产环境。因为我们生成的随机 id 可能会从数据库中删除,或者会在过滤器中被删除。假设 max_id(records) == count(records) 是一种不好的做法。

      (Ofcouce,如果您不删除与查询使用相当的数据百分比,或者如果您不想使用任何过滤器,并且如果您有信心,则可以使用随机 id 进行 random )

      如果你只想要一个项目。 参考(@Valter Silva)

      import random
      
      mgr = models.Painting.objects
      qs = mgr.filter(...)
      random_id = random.choice(1, qs.count())-1        # <--- [ First Query Hit ]
      
      random_paint = qs[random_id] ## <-- [ Second Query Hit ]
      

      如果你想要'n'个项目。

      import random
      
      req_no_of_random_items = 8        ## i need 8 random items.
      qs = models.Painting.objects.filter(...)
      
      ## if u prefer to use random values often, you can keep this in cache. 
      possible_ids = list(qs.values_list('id', flat=True))        # <--- [ First Query Hit ]
      
      possible_ids = random.choices(possible_ids, k=8)
      random_paint = qs.filter(pk__in=possible_ids) ## in a generic case to get 'n' items.
      
      

      或者,如果您想为生产提供更优化的代码,请使用缓存函数来获取产品 ID:

      from django.core.cache import cache
      
      def id_set_cache(qs):
          key = "some_random_key_for_cache"
          id_set =  cache.get(key)
          if id_set is None:
              id_set = list(qs.values_list('id', flat=True)
              cache.set(key, id_set)
          retrun id_set
      

      【讨论】:

      • 当您有数百万行时,即使您只选择 ID,这也会杀死内存。
      【解决方案9】:

      您好,我需要从查询集中选择一条随机记录,我还需要报告其长度(即网页生成了描述的项目并留下了所述记录)

      q = Entity.objects.filter(attribute_value='this or that')
      item_count = q.count()
      random_item = q[random.randomint(1,item_count+1)]
      

      花费了一半的时间(0.7s vs 1.7s):

      item_count = q.count()
      random_item = random.choice(q)
      

      我猜它避免了在选择随机条目之前拉下整个查询,并使我的系统对一个重复访问的页面有足够的响应,用户希望看到 item_count 倒计时。

      【讨论】:

        【解决方案10】:

        主键自增不删除的方法

        如果你有一个表,其中主键是一个没有间隙的连续整数,那么下面的方法应该可以工作:

        import random
        max_id = MyModel.objects.last().id
        random_id = random.randint(0, max_id)
        random_obj = MyModel.objects.get(pk=random_id)
        

        此方法比此处遍历表的所有行的其他方法效率更高。虽然它确实需要两个数据库查询,但两者都是微不足道的。此外,它很简单,不需要定义任何额外的类。但是,它的适用性仅限于具有自动递增主键的表,其中行从未被删除,因此 id 序列中没有间隙。

        如果行已被删除而成为间隙,如果在随机选择现有主键之前重试此方法仍然可以工作。

        参考文献

        【讨论】:

          【解决方案11】:

          请注意一个(相当常见的)特殊情况,如果表中有一个没有删除的索引自动增量列,则执行随机选择的最佳方法是如下查询:

          SELECT * FROM table WHERE id = RAND() LIMIT 1
          

          假设表中有一个名为 id 的列。在 django 中,您可以通过以下方式执行此操作:

          Painting.objects.raw('SELECT * FROM appname_painting WHERE id = RAND() LIMIT 1')
          

          您必须将 appname 替换为您的应用程序名称。

          一般来说,使用 id 列,order_by('?') 可以更快地完成:

          Paiting.objects.raw(
                  'SELECT * FROM auth_user WHERE id>=RAND() * (SELECT MAX(id) FROM auth_user) LIMIT %d' 
              % needed_count)
          

          【讨论】:

            【解决方案12】:

            强烈推荐Getting a random row from a relational database

            因为使用 django orm 做这样的事情,如果你有大数据表,会让你的数据库服务器特别生气:|

            解决方案是提供模型管理器并手动编写 SQL 查询;)

            更新

            无需编写自定义ModelManager 即可在任何数据库后端(甚至是非rel 后端)上工作的另一种解决方案。 Getting Random objects from a Queryset in Django

            【讨论】:

              【解决方案13】:

              您可能希望使用same approach 来对任何迭代器进行采样,尤其是当您计划对多个项目进行采样以创建一个样本集时。 @MatijnPieters 和 @DzinX 对此进行了深思熟虑:

              def random_sampling(qs, N=1):
                  """Sample any iterable (like a Django QuerySet) to retrieve N random elements
              
                  Arguments:
                    qs (iterable): Any iterable (like a Django QuerySet)
                    N (int): Number of samples to retrieve at random from the iterable
              
                  References:
                    @DZinX:  https://stackoverflow.com/a/12583436/623735
                    @MartinPieters: https://stackoverflow.com/a/12581484/623735
                  """
                  samples = []
                  iterator = iter(qs)
                  # Get the first `N` elements and put them in your results list to preallocate memory
                  try:
                      for _ in xrange(N):
                          samples.append(iterator.next())
                  except StopIteration:
                      raise ValueError("N, the number of reuested samples, is larger than the length of the iterable.")
                  random.shuffle(samples)  # Randomize your list of N objects
                  # Now replace each element by a truly random sample
                  for i, v in enumerate(qs, N):
                      r = random.randint(0, i)
                      if r < N:
                          samples[r] = v  # at a decreasing rate, replace random items
                  return samples
              

              【讨论】:

              • Matijn 和 DxinX 的解决方案是针对不提供随机访问的数据集。对于这样做的数据集(SQL 使用OFFSET),这不必要地低效。
              • @EndreBoth 确实如此。我只是喜欢不管数据源如何都使用相同方法的编码“效率”。有时,数据采样效率不会显着影响受其他流程限制的管道的性能(无论您实际对数据做什么,例如 ML 训练)。
              【解决方案14】:

              一种更简单的方法是简单地过滤到感兴趣的记录集并使用random.sample 选择任意数量的记录集:

              from myapp.models import MyModel
              import random
              
              my_queryset = MyModel.objects.filter(criteria=True)  # Returns a QuerySet
              my_object = random.sample(my_queryset, 1)  # get a single random element from my_queryset
              my_objects = random.sample(my_queryset, 5)  # get five random elements from my_queryset
              

              请注意,您应该有一些代码来验证my_queryset 不为空;如果第一个参数包含的元素太少,random.sample 将返回 ValueError: sample larger than population

              【讨论】:

              • 这会导致整个查询集被检索吗?
              • @perrohunter 它甚至不适用于Queryset(至少对于 Python 3.7 和 Django 2.1);您必须先将其转换为列表,这显然会检索整个查询集。
              • @EndreBoth - 这是在 2016 年写的,当时这两个都不存在。
              • 这就是我添加版本信息的原因。但如果它在 2016 年有效,它是通过将整个查询集拉到一个列表中来实现的,对吧?
              • @EndreBoth 正确。
              【解决方案15】:

              我得到了非常简单的解决方案,制作自定义管理器:

              class RandomManager(models.Manager):
                  def random(self):
                      return random.choice(self.all())
              

              然后添加模型:

              class Example(models.Model):
                  name = models.CharField(max_length=128)
                  objects = RandomManager()
              

              现在,你可以使用它了:

              Example.objects.random()
              

              【讨论】:

              • 来自随机导入选择
              • 如果你想要速度,请不要使用这种方法。这个解决方案非常慢。我查过了。比order_by('?').first()慢60倍以上。
              • @Alex78191 不,“?”也很糟糕,但我的方法非常慢。我使用了最佳答案解决方案。
              猜你喜欢
              • 2017-10-04
              • 2020-10-31
              • 2010-12-16
              • 1970-01-01
              • 2022-01-01
              • 1970-01-01
              • 2017-06-05
              • 2022-09-27
              相关资源
              最近更新 更多