Django ORM 中如何随机获取一条记录?

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

我使用的是 Django 1.0.2。

虽然前3个很容易使用django模型,但最后一个(随机)给我带来了一些麻烦。我可以在我的视图中ofc编码它,像这样:

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

在我看来,它不像我想要的东西——这完全是数据库抽象的一部分,应该在模型中。此外,在这里我需要照顾已删除的记录(那么所有记录的数量将不包括我所有可能的键值),可能还有很多其他事情。

还有其他选择吗,最好是在模型抽象中?

105968 次浏览

你可以在你的模型上创建一个经理来做这类事情。首先要理解什么是管理器,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()

简单的使用方法:

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

它被记录在QuerySet API中。

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

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('?')快。

使用order_by('?')将在生产的第二天杀死db服务器。更好的方法是像从关系数据库中获取随机行中描述的那样。

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]

强烈推荐从关系数据库中获取一个随机的行

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

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

更新:

另一种解决方案可以在任何数据库后端工作,甚至是非rel后端,而无需编写自定义ModelManager从Django中的Queryset中获取随机对象

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

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)

我有一个简单的想法:

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

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

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

你可能想要使用用于采样任何迭代器的同样的方法,特别是当你计划对多个项进行采样以创建样本集时。@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

一个更简单的方法是过滤到感兴趣的记录集,并使用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

这里有一个简单的解决方案:

from random import randint


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

嗨,我需要从一个长度的查询集选择一个随机记录,我也需要报告(即网页产生描述的项目,并说记录留下)

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的倒数。

我有一个很简单的解决方案,做客户经理:

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

方法用于在不删除的情况下自动递增主键

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

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

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

在已删除的行存在空白的情况下,如果重试该方法,直到随机选择一个现有主键为止,则该方法仍然可以工作。

参考文献

  • https://stackoverflow.com/a/10836811/4651668 < a href = " https://stackoverflow.com/a/10836811/4651668 " > < / >
  • https://stackoverflow.com/a/2118712/4651668 < a href = " https://stackoverflow.com/a/2118712/4651668 " > < / >
  • https://stackoverflow.com/a/39751708/4651668 < a href = " https://stackoverflow.com/a/39751708/4651668 " > < / >
  • https://github.com/greenelab/hetmech-backend/pull/48

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

  1. 基本上我们有DB的数据,
  2. 我们想使用python的rand函数
  3. 并在后记中从数据库中提取所需的全部数据。

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

(Ofcouce,如果你不删除数据的百分比,这是可比的查询使用,或者如果你不想使用任何类型的过滤器,如果你有信心,随机id,你可以进行随机)

如果你只想要一个项目。 参考(@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.


或者如果你想要一个更优化的代码用于生产,使用cachefunction来获取产品的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

通过本地SQL的快速方法。

用MySQL 8测试。Django .x和Django .x

我从表中取出x个record id's的随机集。通过这些id,我获取对象。

例子:

sql = "SELECT id FROM paintings ORDER BY RAND() LIMIT 1"
list_of_ids = [p.id for p in Painting.objects.raw(sql)]
list_of_random_paintings = Painting.objects.filter(id__in=list_of_ids).all()
  • 通过一个可选的WHERE,你可以改变你的查询范围
  • LIMIT设置为1,如果您需要更多的记录,只需更改它

快速基准测试:

  • 107.251记录~ 0.13秒
  • 931.103记录~ 0.8秒
  • 8.044,965记录~ 3.4秒

桌子越大,花的时间就越长。毕竟,使用它取决于您的用例。