我有一个模型代表我在我的网站上展示的画作。在主网页上,我想展示其中的一些:最新的、大部分时间未访问的、最受欢迎的和随机的。
我正在使用 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)
它看起来不像我想要的东西 - 这完全是数据库抽象的一部分,应该在模型中。另外,在这里我需要处理已删除的记录(然后所有记录的数量不会涵盖我所有可能的键值)以及可能还有很多其他事情。
任何其他选项我可以如何做到这一点,最好是在模型抽象中以某种方式?
order_by('?')
使用 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()
有什么好处
.all()[randint(0, count - 1)]
很有趣。也许您应该专注于找出答案的哪一部分是错误的或薄弱的,而不是为我们重新定义“一个错误”,并对愚蠢的选民大喊大叫。 (也许是它没有使用 .objects
?)
如果您使用 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('?') 快。
这是一个简单的解决方案:
from random import randint
count = Model.objects.count()
random_object = Model.objects.all()[randint(0, count - 1)] #single random object
您可以在模型上创建一个 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()
。
其他答案可能很慢(使用 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()
并省去原始查询。
只是一个简单的想法,我是如何做到的:
def _get_random_service(self, professional):
services = Service.objects.filter(professional=professional)
i = randint(0, services.count()-1)
return services[i]
DB 中的随机化在 python 中感觉很糟糕但更好。但同时,为了忽略大部分结果(尤其是在生产环境中),将所有数据从 DB 带到 python 内存并不是一个好主意。我们可能还需要某种过滤。
所以基本上我们在 DB 有数据,我们想使用 python 的 rand 函数,然后从 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
嗨,我需要从查询集中选择一条随机记录,我还需要报告其长度(即网页生成了描述的项目并留下了所述记录)
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 倒计时的重复性任务。
不删除的自动递增主键的方法
如果您有一个表,其中主键是一个没有间隙的连续整数,那么以下方法应该有效:
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
https://stackoverflow.com/a/2118712/4651668
https://stackoverflow.com/a/39751708/4651668
https://github.com/greenelab/hetmech-backend/pull/48
只是要注意一个(相当常见的)特殊情况,如果表中有一个没有删除的索引自动增量列,则执行随机选择的最佳方法是如下查询:
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)
这是强烈推荐 Getting a random row from a relational database
因为使用 django orm 做这样的事情,如果你有大数据表,会让你的数据库服务器特别生气:|
解决方案是提供模型管理器并手动编写 SQL 查询;)
更新:
另一种解决方案适用于任何数据库后端,即使是非 rel 后端也无需编写自定义 ModelManager
。 Getting Random objects from a Queryset in Django
您可能希望使用用于对任何迭代器进行采样的 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
OFFSET
),这是不必要的低效。
一种更简单的方法是简单地过滤到感兴趣的记录集并使用 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
。
Queryset
(至少对于 Python 3.7 和 Django 2.1);您必须先将其转换为列表,这显然会检索整个查询集。
我得到了非常简单的解决方案,制作自定义管理器:
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 多倍。
不定期副业成功案例分享
random.choice(Model.objects.all())
?