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


176

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

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

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

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


На мой взгляд, то, как вы отображаете вещи и какие вещи вы отображаете, является частью уровня "View" или бизнес-логики, которая должна идти на уровне "Controller" в MVC.
Габриэле Д'Антона

В Django контроллером является представление. docs.djangoproject.com/en/dev/faq/general/...

Ответы:


169

Использование 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]

45
Каковы преимущества model.objects.aggregate(count=Count('id'))['count']болееmodel.objects.all().count()
Райан Сакс

11
Хотя это намного лучше, чем принятый ответ, обратите внимание, что этот подход делает два SQL-запроса. Если между ними меняется число, возможно, возникнет ошибка выхода за границы.
Нело Митраним,

2
Это неправильное решение. Это не будет работать, если ваши идентификаторы не начинаются с 0. А также, когда идентификаторы не являются смежными. Скажем, первая запись начинается с 500, а последняя - 599 (при условии непрерывности). Тогда счет будет 54950. Конечно, список [54950] не существует, потому что длина вашего запроса равна 100. Он выбрасывает индекс из связанной исключительной ситуации. Я не знаю, почему так много людей проголосовали за это, и это было отмечено как принятый ответ.
Саджид

1
@sajid: Почему именно ты спрашиваешь меня? Довольно легко увидеть общую сумму моих вкладов в этот вопрос: редактировать ссылку, чтобы указать на архив после того, как он сгнил. Я даже не голосовал ни по одному из ответов. Но я нахожу забавным, что этот ответ и тот, который, как вы утверждаете, намного лучше, используют .all()[randint(0, count - 1)]в действительности. Возможно, вам следует сосредоточиться на выявлении того, какая часть ответа является неправильной или слабой, а не на том, чтобы переопределить для нас «ошибочную ситуацию» и кричать на глупых избирателей. (Может быть, это то, что он не использует .objects?)
Натан Тагги

3
@NathanTuggy. Ок мой плохой Извините
Саджид

260

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

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

Это задокументировано в QuerySet API .


71
Обратите внимание, что этот подход может быть очень медленным, как задокументировано :)
Николас Думазет

6
«может быть дорогим и медленным, в зависимости от используемой вами базы данных». - опыт работы с бэкэндами разных БД? (SQLite / MySQL / Postgres)?
Кендер

4
Я не проверял это, так что это чистое предположение: почему это должно быть медленнее, чем извлечение всех элементов и выполнение рандомизации в Python?
Мухук

8
Я читал, что в MySQL это происходит медленно, так как в MySQL невероятно неэффективное случайное упорядочение.
Брэндон Генри

33
Почему не просто random.choice(Model.objects.all()) ?
Джейми

25

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


30
К сожалению, это далеко не случайно. Если у вас есть запись с идентификатором 1, а другая с идентификатором 100, тогда она вернет вторую в 99% случаев.
DS.

16

Вот простое решение:

from random import randint

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

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] не страдает от этой проблемы и не менее эффективно.
Даниэль Розман

Я понял это, поэтому мой пример просто копирует код вопроса с менеджером. Это все еще будет зависеть от ОП, чтобы решить его проверку границ.
Совют

1
вместо использования .get (id = random_index) не лучше ли использовать .filter (id__gte = random_index) [0: 1]? Во-первых, это помогает решить проблему с непоследовательными ПК. Во-вторых, get_query_set должен вернуть ... QuerySet. И в вашем примере это не так.
Николас Думазет

2
Я бы не стал создавать нового менеджера только для размещения одного метода. Я бы добавил «get_random» в менеджер по умолчанию, чтобы вам не приходилось проходить через обруч all () [0] каждый раз, когда вам нужно случайное изображение. Кроме того, если бы автор был ForeignKey для модели User, вы могли бы сказать user.painting_set.get_random ().
Антти Расинен

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

6

Другие ответы являются либо потенциально медленными (используются 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]

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


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

2

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

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

1

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

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

который предполагает такой столбец с именем id для таблицы. В Django вы можете сделать это:

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

в котором вы должны заменить имя приложения на имя вашего приложения.

В общем, с помощью столбца id, order_by ('?') Можно сделать намного быстрее с помощью:

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

1

Настоятельно рекомендуется получить случайную строку из реляционной базы данных

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

И решение состоит в том, чтобы предоставить Менеджер моделей и написать SQL-запрос вручную;)

Обновить :

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


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 Both

@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 Both

@EndreBoth - это было написано в 2016 году, когда ни один из них не существовал.
эйканал

Вот почему я добавил информацию о версии. Но если он работал в 2016 году, он сделал это, потянув весь набор запросов в список, верно?
Endre Both

@EndreBoth Правильно.
эйканал

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.


0

Метод автоматического увеличения первичного ключа без удалений

Если у вас есть таблица, в которой первичный ключ представляет собой последовательное целое число без пробелов, тогда должен работать следующий метод:

import random
max_id = MyModel.objects.last().id
random_id = random.randint(0, max_id)
random_obj = MyModel.objects.get(pk=random_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()

от случайного выбора импорта
Адам Старр

3
Пожалуйста, не используйте этот метод, если вы хотите скорость. Это решение очень медленное. Я проверил Это медленнее, чем order_by('?').first()более чем в 60 раз.
LagRange

@ Alex78191 нет, "?" тоже плохо, но мой метод очень медленный. Я использовал топ-решение ответа.
LagRange
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.