如何使用Django的ORM提取随机记录?


181

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

我正在使用Django 1.0.2。

尽管使用django模型可以轻松提取其中的前3个,但最后一个(随机)会给我带来一些麻烦。在我看来,我可以将代码进行如下编码:

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的“视图”层或业务逻辑的一部分,应该在MVC的“控制器”层中进行。
加布里埃莱·德安东尼奥(Jubric)2009年


为此应该有一个内置功能-一个不使用的功能order_by('?')
user-124812948

Answers:


176

使用order_by('?')将在生产的第二天杀死数据库服务器。更好的方法类似于“从关系数据库获取随机行”中所述

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]

46
model.objects.aggregate(count=Count('id'))['count']结束带来了什么好处model.objects.all().count()
Ryan Saxe 2014年

11
尽管比接受的答案好得多,但请注意,此方法可以进行两个SQL查询。如果计数在两者之间变化,则可能会出现超出范围的错误。
Nelo Mitranim

2
这是一个错误的解决方案。如果您的ID并非从0开始,那么该方法将无法正常工作。此外,当ID不连续时,也无法使用。假设第一个记录从500开始,最后一个记录是599(假设连续)。然后,计数将为54950。肯定是list [54950]不存在,因为查询的长度为100。它将使索引超出绑定异常。我不知道为什么有这么多人赞成这个,这被标记为可接受的答案。
萨吉德(Sajid)

1
@sajid:到底为什么要问我?很容易看出我对这个问题的贡献之和:编辑一个指向烂掉的档案的链接。我什至没有对任何答案进行投票。但是,我确实觉得很有趣,这个答案和您声称要好得多的答案都.all()[randint(0, count - 1)]有效地使用了。也许您应该专注于确定答案的哪一部分是错误的还是错误的,而不是为我们重新定义“一次错误”,然后对愚蠢的选民大喊大叫。(也许是因为它没有使用.objects?)
Nathan Tuggy

3
@NathanTuggy。好吧,我的坏。抱歉
Sajid

266

只需使用:

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

它记录在QuerySet API中


75
请注意,如所记录的那样,这种方法可能非常慢:)
Nicolas Dumazet 09年

6
“可能昂贵且缓慢,具体取决于您所使用的数据库后端。” -对不同的DB后端有任何经验吗?(sqlite / mysql / postgres)?
kender

4
我还没有测试过,所以这纯粹是猜测:为什么它比在Python中检索所有项目并执行随机化要慢?
muhuk

8
我读到它在mysql中运行缓慢,因为mysql的随机排序效率极低。
布兰登·亨利

34
为什么不只是random.choice(Model.objects.all())呢?
杰米

25

如果使用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('?')快。


31
而且,可悲的是,它并不是随机的。如果您有一条ID为1的记录,而另一条ID为100的记录,则它将在99%的时间内返回第二条记录。
DS。


10

您可以在模型上创建一个经理来执行此类操作。首先要明白一个经理是什么,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()


4
仅当您的pk是连续的时,才使用get()将起作用,即,您永远不会删除任何项目。否则,您可能会尝试获取不存在的pk。使用.all()[random_index]不会遇到此问题,并且效率也不低。
丹尼尔·罗斯曼2009年

我理解这就是为什么我的示例仅向经理复制问题代码的原因。仍然要由OP来进行边界检查。
苏联2009年

1
与其使用.get(id = random_index)而不是使用.filter(id__gte = random_index)[0:1]更好?首先,它有助于解决非连续pk的问题。其次,get_query_set应该返回... QuerySet。在您的示例中,事实并非如此。
Nicolas Dumazet 09年

2
我不会仅仅为了容纳一种方法而创建新的经理。我将“ get_random”添加到默认管理器中,以便您每次需要随机映像时都不必经历all()[0]循环。此外,如果作者是用户模型的外键,则可以说user.painting_set.get_random()。
Antti Rasinen 09年

我通常会在需要采取全面行动(例如获取随机记录列表)时创建新的经理。如果我要对已有的记录执行更具体的任务,则会在默认管理器上创建一个方法。
苏联2009年

7

其他答案可能很慢(使用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()

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


一个很好的概念证明,但这也是数据库内部的两个查询,您保存的是数据库的一次往返。您必须执行很多次才能使编写和维护原始查询值得。而且,如果您要防止出现空表,则最好先运行acount()并省去原始查询。
Endre双方

真的值得。
Jerrychayan

3

只是一个简单的想法,我该怎么做:

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

2

嗨,我需要从查询集中选择一个随机记录,该记录的长度我也需要报告(即,网页生成了描述项,并且记录还剩下)

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

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

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

我猜想它可以避免在选择随机条目之前拉低整个查询,并使我的系统对于足以重复访问某个页面的页面有足够的响应能力,以使用户希望减少item_count的计数。


2

DB中的随机化在python中令人讨厌和更好。但是同时,将所有数据从数据库带到python内存只是忽略大多数结果(尤其是在生产环境中)并不是一个好主意。我们可能还需要某种过滤。

  1. 所以基本上我们在DB有数据,
  2. 我们想使用python的rand函数
  3. 后记会从DB提供所有必需的数据。

基本上,使用2个查询比在DB CPU中随机选择(在DB中计算)或加载整个数据(繁重的网络利用率)要便宜得多。解释的解决方案必须具有可伸缩性,因此尝试在此处进行计划将不适用于特别是带有过滤器,软/硬删除甚至带有is_public标志的生产环境。因为我们生成的随机ID可能会从数据库中删除或在过滤器中被删减。假定max_id(records)== count(records)是一个坏习惯。

(当然,如果您不删除与查询使用的数据相当的百分比,或者您不想使用任何种类的过滤器,并且如果您有信心,可以使用random 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

1

仅需注意(一种非常常见的)特殊情况,如果表中有一个索引自动递增列且没有删除,那么执行随机选择的最佳方法是查询,例如:

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)


1

您可能想要使用对任何迭代器进行抽样相同的方法,尤其是如果您打算对多个项目进行抽样以创建样本集时,尤其如此。@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),这不必要地效率低下。
Endre双方

确实是@EndreBoth。我只是喜欢使用相同方法的编码“效率”,而与数据源无关。有时,数据采样效率不会显着影响受其他流程限制的管道的性能(无论您实际上是如何处理数据,例如ML训练)。
滚刀

1

一种更简单的方法包括简单地过滤到感兴趣的记录集,并根据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


2
这会导致整个查询集被检索吗?
perrohunter

@perrohunter甚至无法使用Queryset(至少在Python 3.7和Django 2.1下);您必须先将其转换为列表,这显然会检索整个查询集。
Endre双方

@EndreBoth-这是在2016年编写的,当时这两个都不存在。
eykanal

这就是为什么我添加了版本信息。但是,如果它在2016年行得通,那就是通过将整个查询集放入一个列表中来实现的,对吗?
Endre双方

@EndreBoth正确。
eykanal

1

自动删除不删除主键的方法

如果您有一个表,其中主键是一个没有间隔的连续整数,那么以下方法应该有效:

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

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

在删除行(例如空格)的情况下,如果重试该方法直到随机选择一个现有的主键,该方法仍然可以使用。

参考文献


0

我有一个非常简单的解决方案,使自定义经理:

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

从随机的进口选择
亚当·斯塔吉尔19-4-23

3
请不要使用此方法,如果您想提高速度。这个解决方案非常慢。我查过 它慢于order_by('?').first()60倍以上。
LagRange

@ Alex78191不,“?” 也很糟糕,但是我的方法非常慢。我使用了最佳答案解决方案。
LagRange
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.