Як витягнути випадковий запис за допомогою ORM Django?


176

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

Я використовую Django 1.0.2.

Хоча перші 3 з них легко витягти за допомогою моделей джанго, остання (випадкова) завдає мені певних проблем. Я можуcc кодувати це, на мій погляд, приблизно так:

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, на мою думку.
Габріеле Д'Антона

У Django контролером є вид. docs.djangoproject.com/uk/dev/faq/general/…

Відповіді:


169

Використання 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]

45
Які переваги model.objects.aggregate(count=Count('id'))['count']надmodel.objects.all().count()
Ryan Saxe

11
Хоча набагато краще, ніж прийнята відповідь, зауважте, що такий підхід робить два запити SQL. Якщо підрахунок зміниться між ними, можливо, вийде помилка поза межами.
Нело Мітранім

2
Це неправильне рішення. Це не спрацює, якщо ваші ідентифікатори не почнуть починати з 0. А також, коли ідентифікатори не будуть суміжними. Скажімо, перший запис починається з 500, а останній - 599 (якщо припускати суміжність). Тоді кількість буде 54950. Напевно, список [54950] не існує, оскільки довжина вашого запиту дорівнює 100. Він викине індекс із обмеженого винятку. Я не знаю, чому так багато людей схвалили це, і це було позначено як прийняту відповідь.
sajid

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

3
@NathanTuggy. Гаразд мій поганий. Вибачте
sajid

260

Просто використовуйте:

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

Це задокументовано в API QuerySet .


71
Зауважте, що такий підхід може бути дуже повільним, як це зафіксовано :)
Ніколас Думазет

6
"може бути дорогим і повільним, залежно від бази даних, яку ви використовуєте." - будь-який досвід роботи з різними пакетами БД? (sqlite / mysql / postgres)?
Кендер

4
Я не перевіряв його, тому це чиста спекуляція: чому це повинно бути повільніше, ніж вилучення всіх елементів та проведення рандомізації в Python?
muhuk

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
Також, на жаль, це далеко не випадково. Якщо у вас є запис з id 1 і інший з id 100, він поверне другий 99% часу.
ДС.

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()і т.д. Створення власного менеджера дозволяє попередньо фільтр результати і мають всі ті ж самі методи, а також свої власні методи, призначені для користувача роботи за результатами .

EDIT : Я змінив код, щоб відобразити 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 () буде працювати лише в тому випадку, якщо ваші ПК послідовні, тобто ви ніколи не видаляєте жодних елементів. В іншому випадку ви, ймовірно, спробуєте отримати ПК, який не існує. Використання .all () [random_index] не страждає від цієї проблеми і не менш ефективно.
Даніель Роузмен

Я зрозумів, що саме тому мій приклад просто копіює код питання з менеджером. Досі ОП буде відпрацьовувати перевірку своїх меж.
Радянський

1
замість використання .get (id = random_index) не буде краще використовувати .filter (id__gte = random_index) [0: 1]? По-перше, це допомагає вирішити проблему з непослідовними ПК. По-друге, get_query_set повинен повернути ... QuerySet. І у вашому прикладі це не так.
Ніколя Думазет

2
Я б не створив нового менеджера просто для використання одного методу. Я додаю "get_random" до менеджера за замовчуванням, щоб вам не довелося проходити обруч all () [0] щоразу, коли вам потрібно випадкове зображення. Крім того, якби автор був іноземним ключем до моделі користувача, ви можете сказати 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 обидва

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

що передбачає такий стовпець з ідентифікатором для таблиці. У джанго ви можете це зробити:

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. Отримання випадкових об'єктів із набору запитів у 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 Обидва

@EndreBoth справді. Мені просто подобається «ефективність» кодування використання одного і того ж підходу незалежно від джерела даних. Іноді ефективність відбору даних не суттєво впливає на продуктивність трубопроводу, обмеженого іншими процесами (що б ви насправді не робили з даними, як, наприклад, навчання МЛ).
варильні панелі

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 році, коли жодного з них не існувало.
ейканал

Тому я додав інформацію про версію. Але якщо це працювало в 2016 році, це було зроблено, перетягнувши весь набір запитів у список, правда?
Endre обидва

@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,7s проти 1,7s), як:

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 ні, "?" теж погано, але мій метод EXTRA повільний. Я застосував верхнє рішення відповіді.
LagRange
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.