Как вытащить случайную запись, используя ORM Джанго?

168

У меня есть модель, которая представляет картины, которые я представляю на своем сайте. На главной веб-странице я хотел бы показать некоторые из них: новейшие, которые не были посещены в большинстве случаев, самые популярные и случайные.

Я использую 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)

Это не похоже на то, что я хотел бы иметь на мой взгляд. Это - полностью часть абстракции базы данных и должна быть в модели. Кроме того, здесь мне нужно позаботиться об удаленных записях (тогда число всех записей не покрывает меня всеми возможными значениями ключа) и, вероятно, много других вещей.

Любые другие варианты, как я могу это сделать, желательно как-то внутри абстракции модели?

  • 0
    По моему мнению, то, как вы отображаете вещи, и какие вещи вы отображаете, является частью уровня «View» или бизнес-логики, которая должна идти на уровне «Controller» в MVC.
  • 0
    В Django контроллером является представление. docs.djangoproject.com/en/dev/faq/general/...
Теги:
django-models

15 ответов

139
Лучший ответ

Использование 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]
  • 39
    Каковы преимущества model.objects.aggregate(count=Count('id'))['count'] сравнению с model.objects.all().count()
  • 8
    Хотя это намного лучше, чем принятый ответ, обратите внимание, что этот подход делает два SQL-запроса. Если между ними меняется число, возможно, возникнет ошибка выхода за границы.
Показать ещё 11 комментариев
244

Просто используйте:

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

Документировано в API QuerySet.

  • 61
    Обратите внимание, что этот подход может быть очень медленным, как задокументировано :)
  • 5
    «может быть дорогим и медленным, в зависимости от используемой вами базы данных». - опыт работы с бэкэндами разных БД? (SQLite / MySQL / Postgres)?
Показать ещё 11 комментариев
27

Решения с 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 ('?') почти во всех случаях.

  • 0
    Да, но этот подход не работает с наборами запросов.
  • 29
    К сожалению, это далеко не случайно. Если у вас есть запись с идентификатором 1, а другая - с идентификатором 100, вторая вернет 99% времени.
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().

  • 3
    Использование get () будет работать только в том случае, если ваши pks последовательны, то есть вы никогда не удаляете какие-либо элементы. В противном случае вы, вероятно, попытаетесь получить ПК, который не существует. Использование .all () [random_index] не страдает от этой проблемы и не менее эффективно.
  • 0
    Я понял это, поэтому мой пример просто копирует код вопроса с менеджером. Все еще будет зависеть от ОП, чтобы решить его проверку границ.
Показать ещё 3 комментария
8

Здесь простое решение:

from random import randint

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

Другие ответы либо потенциально медленны (используя order_by('?')), либо используют более одного SQL-запроса. Здесь примерное решение без упорядочения и только один запрос (при условии Postgres):

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

Помните, что это приведет к ошибке индекса, если таблица пуста. Напишите себе вспомогательную функцию model-agnostic, чтобы проверить это.

  • 0
    Хорошее подтверждение концепции, но это также два запроса в базе данных, и вы сохраняете один обходной путь к базе данных. Вы должны выполнить это много раз, чтобы написать и поддерживать необработанный запрос. И если вы хотите защититься от пустых таблиц, вы также можете заранее запустить count() и отказаться от необработанного запроса.
2

Я создал модель менеджера

models.py (пример)

from django.db import models

class RandomManager(models.Manager):

  def get_random(self, items=1):
    '''
    items is integer value
    By default it returns 1 random item
    '''
    if isinstance(items, int):
        return self.model.objects.order_by('?')[:items]
    return self.all()


class Category(models.Model):
  name = models.CharField(max_length=100)

  objects = RandomManager()

  class Meta:
    default_related_name = 'categories'
    verbose_name = 'category'
    verbose_name_plural = 'categories'

И вы можете получить случайные предметы из базы данных, например

Category.objects.get_random(5) #  To get 5 random items 
  • 0
    Работает отлично, я не проверял скорость, хотя. Как вы думаете, будут ли проблемы со скоростью?
  • 0
    Я не тестировал большой набор запросов
Показать ещё 1 комментарий
2

Просто простую идею, как я это делаю:

def _get_random_service(self, professional):
    services = Service.objects.filter(professional=professional)
    i = randint(0, services.count()-1)
    return services[i]
  • 0
    Это не будет работать, если ваши идентификаторы не являются смежными
  • 0
    @ Патрик Да, это будет; [i] не относится к идентификатору строки.
1

Привет, мне нужно было выбрать случайную запись из набора запросов, длина которой мне также нужно было сообщить (т.е. Веб-страница произвела описанный элемент и оставила записи)

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

потребовалось вдвое меньше (0,7 с против 1,7 с), как:

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

Я предполагаю, что это избегает сносить весь запрос перед выбором случайной записи и делает мою систему достаточно отзывчивой для страницы, к которой обращаются неоднократно для повторяющейся задачи, где пользователи хотят видеть обратный отсчет item_count.

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
    Приведет ли это к получению всего набора запросов?
  • 0
    @perrohunter Он даже не будет работать с Queryset (по крайней мере, с Python 3.7 и Django 2.1); Вы должны сначала преобразовать его в список, который, очевидно, извлекает весь набор запросов.
Показать ещё 3 комментария
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:  /questions/44847/python-random-sample-with-a-generator-iterable-iterator/320738#320738
      @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
  • 0
    Решение Matijn и DxinX предназначено для наборов данных, которые не обеспечивают произвольный доступ. Для наборов данных, которые делают (и SQL делает с OFFSET ), это излишне неэффективно.
  • 0
    @EndreBoth действительно. Мне просто нравится «эффективность» кодирования при использовании одного и того же подхода независимо от источника данных. Иногда эффективность выборки данных не оказывает существенного влияния на производительность конвейера, ограниченного другими процессами (независимо от того, что вы на самом деле делаете с данными, например, обучение ML).
1

Это высоко рекомендуется Получение случайной строки из реляционной базы данных

Поскольку использование django orm для выполнения подобной вещи сделает ваш сервер db сердитым специально, если у вас есть большая таблица данных: |

И решение предоставляет диспетчер моделей и записывает запрос SQL вручную;)

Обновление

Другое решение, которое работает на любом бэкэнде базы данных, даже не относящихся к нему, без написания пользовательского ModelManager. Получение случайных объектов из Queryset в Django

0

Я получил очень простое решение, сделать собственный менеджер:

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

а затем добавить в модель:

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

Теперь вы можете использовать его:

Example.objects.random()
0

Что не так просто:

import random
records = Model.objects.all()
random_record = random.choice(records)
0

Просто отметим (довольно распространенный) специальный случай, если в таблице есть индексированный столбец автоинкремента, без удаления, оптимальным способом случайного выбора является запрос типа:

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)

Ещё вопросы

Сообщество Overcoder
Наверх
Меню