Jak pobrać losowy rekord za pomocą ORM Django?

176

Mam model przedstawiający obrazy, które prezentuję na mojej stronie. Na głównej stronie chciałbym pokazać kilka z nich: najnowsze, najczęściej nie odwiedzane, najpopularniejsze i przypadkowe.

Używam Django 1.0.2.

Podczas gdy pierwsze 3 z nich są łatwe do ściągnięcia za pomocą modeli django, ostatnia (losowa) sprawia mi trochę problemów. Moim zdaniem mogę to ofc zakodować na coś takiego:

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

Nie wygląda to na coś, co chciałbym mieć w mojej opinii - jest to całkowicie część abstrakcji bazy danych i powinno znajdować się w modelu. Również tutaj muszę zająć się usuniętymi rekordami (wtedy liczba wszystkich rekordów nie obejmie wszystkich możliwych wartości kluczowych) i prawdopodobnie wiele innych rzeczy.

Jakieś inne opcje, jak mogę to zrobić, najlepiej jakoś wewnątrz abstrakcji modelu?

kender
źródło
To, jak wyświetlasz rzeczy i jakie rzeczy wyświetlasz, jest częścią poziomu „Widoku” lub logiki biznesowej, która moim zdaniem powinna znajdować się na poziomie „Kontrolera” MVC.
Gabriele D'Antona
W Django kontrolerem jest widok. docs.djangoproject.com/en/dev/faq/general/…

Odpowiedzi:

169

Użycie order_by('?')spowoduje zabicie serwera db drugiego dnia produkcji. Lepszym sposobem jest coś, co opisano w sekcji Pobieranie losowego wiersza z relacyjnej bazy danych .

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]
Emil Ivanov
źródło
45
Jakie są zalety model.objects.aggregate(count=Count('id'))['count']ponadmodel.objects.all().count()
Ryan Saxe
11
Chociaż jest to znacznie lepsze niż akceptowana odpowiedź, zauważ, że takie podejście powoduje dwa zapytania SQL. Jeśli liczba zmieni się w międzyczasie, może być możliwe uzyskanie błędu poza zakresem.
Nelo Mitranim,
2
To jest złe rozwiązanie. Nie zadziała, jeśli Twoje identyfikatory nie będą zaczynać się od 0. A także wtedy, gdy identyfikatory nie są ciągłe. Powiedzmy, pierwszy rekord zaczyna się od 500, a ostatni to 599 (zakładając ciągłość). Wtedy liczba wyniesie 54950. Z pewnością lista [54950] nie istnieje, ponieważ długość twojego zapytania wynosi 100. Spowoduje to wyrzucenie indeksu poza powiązany wyjątek. Nie wiem, dlaczego tak wiele osób głosowało za tym i zostało to oznaczone jako zaakceptowana odpowiedź.
sajid
1
@sajid: Dlaczego dokładnie mnie o to pytasz? Całkiem łatwo jest zobaczyć sumę moich wkładów w to pytanie: edycję linku tak, aby wskazywał na archiwum po jego zgniyciu. Nawet nie głosowałem na żadną z odpowiedzi. Ale wydaje mi się zabawne, że zarówno ta odpowiedź, jak i ta, o której twierdzisz, że jest znacznie lepsza, .all()[randint(0, count - 1)]w efekcie wykorzystują . Może powinieneś skupić się na określeniu, która część odpowiedzi jest błędna lub słaba, zamiast na nowo definiować dla nas „jeden błąd” i krzyczeć na głupich wyborców. (Może to dlatego, że nie używa .objects?)
Nathan Tuggy
3
@NathanTuggy. Ok, moja wina. Przepraszam
sajid
260

Po prostu użyj:

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

Jest to udokumentowane w QuerySet API .

muhuk
źródło
71
Należy pamiętać, że takie podejście może być bardzo powolne, jak udokumentowano :)
Nicolas Dumazet
6
„może być kosztowne i wolne, w zależności od używanego zaplecza bazy danych”. - jakieś doświadczenie z różnymi backendami DB? (sqlite / mysql / postgres)?
kender
4
Nie testowałem tego, więc to czysta spekulacja: dlaczego miałoby to być wolniejsze niż pobieranie wszystkich pozycji i przeprowadzanie randomizacji w Pythonie?
muhuk
8
Czytałem, że w mysql działa wolno, ponieważ mysql ma niewiarygodnie nieefektywne losowe porządkowanie.
Brandon Henry
33
Dlaczego nie po prostu random.choice(Model.objects.all())?
Jamey
25

Rozwiązania z order_by ('?') [: N] są ekstremalnie powolne, nawet dla tabel średniej wielkości, jeśli używasz MySQL (nie wiem o innych bazach danych).

order_by('?')[:N]zostaną przetłumaczone na SELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT Nzapytanie.

Oznacza to, że dla każdego wiersza w tabeli zostanie wykonana funkcja RAND (), a następnie cała tabela zostanie posortowana według wartości tej funkcji i zostanie zwróconych pierwszych N rekordów. Jeśli twoje stoły są małe, to w porządku. Ale w większości przypadków jest to bardzo powolne zapytanie.

Napisałem prostą funkcję, która działa nawet jeśli id ​​mają dziury (niektóre wiersze zostały usunięte):

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]

W prawie wszystkich przypadkach jest szybszy niż order_by („?”).

Michaił Korobow
źródło
30
Niestety, nie jest to przypadkowe. Jeśli masz rekord o identyfikatorze 1 i inny o identyfikatorze 100, to w 99% przypadków zwróci on drugi rekord.
DS.
16

Oto proste rozwiązanie:

from random import randint

count = Model.objects.count()
random_object = Model.objects.all()[randint(0, count - 1)] #single random object
Maulik Patel
źródło
10

Możesz stworzyć menedżera na swoim modelu, aby robić tego typu rzeczy. Aby najpierw zrozumieć, co menedżer to, że Painting.objectsmetoda jest menedżerem, który zawiera all(), filter(), get(), itd. Tworzenie własnego menedżera pozwala na filtr wstępny wyniki i mają wszystkie te same metody, jak również własne metody niestandardowe prace nad wynikami .

EDYCJA : zmodyfikowałem mój kod, aby odzwierciedlić order_by['?']metodę. Zwróć uwagę, że menedżer zwraca nieograniczoną liczbę losowych modeli. Z tego powodu dołączyłem trochę kodu użycia, aby pokazać, jak uzyskać tylko jeden model.

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.

Stosowanie

random_painting = Painting.randoms.all()[0]

Wreszcie, możesz mieć wielu menedżerów w swoich modelach, więc możesz utworzyć LeastViewsManager()lub MostPopularManager().

Soviut
źródło
3
Użycie get () działałoby tylko wtedy, gdy pakiety są następujące po sobie, tj. Nigdy nie usuwasz żadnych elementów. W przeciwnym razie prawdopodobnie spróbujesz zdobyć pakiet, który nie istnieje. Używanie .all () [random_index] nie powoduje tego problemu i nie jest mniej wydajne.
Daniel Roseman
Zrozumiałem to, dlatego mój przykład po prostu powiela kod pytania z menedżerem. To nadal będzie zależało od PO, aby sprawdzić swoje granice.
Soviut
1
zamiast używać .get (id = random_index) nie byłoby lepiej użyć .filter (id__gte = random_index) [0: 1]? Po pierwsze, pomaga rozwiązać problem z niekolejnymi pakietami. Po drugie, get_query_set powinno zwrócić ... QuerySet. W twoim przykładzie tak nie jest.
Nicolas Dumazet
2
Nie stworzyłbym nowego menedżera tylko po to, by pomieścić jedną metodę. Dodałbym "get_random" do domyślnego menedżera, abyś nie musiał przechodzić przez pętlę all () [0] za każdym razem, gdy potrzebujesz losowego obrazu. Ponadto, jeśli autor byłby kluczem obcym do modelu użytkownika, można by powiedzieć user.painting_set.get_random ().
Antti Rasinen
Zwykle tworzę nowego menedżera, gdy potrzebuję ogólnej akcji, takiej jak uzyskanie listy losowych rekordów. Utworzyłbym metodę na domyślnym menedżerze, gdybym wykonywał bardziej szczegółowe zadanie z już posiadanymi rekordami.
Soviut
6

Pozostałe odpowiedzi są albo potencjalnie wolne (używają order_by('?')), albo używają więcej niż jednego zapytania SQL. Oto przykładowe rozwiązanie bez porządkowania i tylko z jednym zapytaniem (zakładając Postgres):

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

Należy pamiętać, że spowoduje to błąd indeksu, jeśli tabela jest pusta. Napisz sobie funkcję pomocniczą niezależną od modelu, aby to sprawdzić.

Nelo Mitranim
źródło
Niezły dowód słuszności koncepcji, ale są to również dwa zapytania w bazie danych, a zapisywane jest jedno przejście do bazy danych. Musiałbyś wykonywać to wiele razy, aby pisanie i utrzymywanie surowego zapytania było tego warte. A jeśli chcesz zabezpieczyć się przed pustymi tabelami, równie dobrze możesz uruchomić z count()wyprzedzeniem i zrezygnować z nieprzetworzonego zapytania.
Endre Both
2

Prosty pomysł, jak to robię:

def _get_random_service(self, professional):
    services = Service.objects.filter(professional=professional)
    i = randint(0, services.count()-1)
    return services[i]
Valter Silva
źródło
1

Wystarczy zwrócić uwagę na (dość powszechny) przypadek specjalny, jeśli w tabeli znajduje się zindeksowana kolumna z automatyczną inkrementacją bez usuwania, optymalnym sposobem wykonania losowego wyboru jest zapytanie takie jak:

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

która zakłada taką kolumnę o nazwie id dla tabeli. W django możesz to zrobić poprzez:

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

w którym musisz zastąpić appname nazwą swojej aplikacji.

Ogólnie rzecz biorąc, z kolumną id, order_by („?”) Można zrobić znacznie szybciej za pomocą:

Paiting.objects.raw(
        'SELECT * FROM auth_user WHERE id>=RAND() * (SELECT MAX(id) FROM auth_user) LIMIT %d' 
    % needed_count)
Amir Ali Akbari
źródło
1

Jest to wysoce zalecane Pobieranie losowego wiersza z relacyjnej bazy danych

Ponieważ użycie django orm do zrobienia czegoś takiego wkurzy twój serwer db, szczególnie jeśli masz tabelę dużych zbiorów danych: |

Rozwiązaniem jest udostępnienie Model Managera i ręczne napisanie zapytania SQL;)

Aktualizacja :

Kolejne rozwiązanie, które działa na dowolnej bazie danych, nawet nie-relacyjnej, bez pisania niestandardowego ModelManager. Pobieranie losowych obiektów z Queryset w Django

Alireza Savand
źródło
1

Możesz chcieć użyć tego samego podejścia , którego używałbyś do próbkowania dowolnego iteratora, zwłaszcza jeśli planujesz próbkować wiele elementów, aby utworzyć zestaw próbek . @MatijnPieters i @DzinX dużo przemyśleli:

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
płyty
źródło
Rozwiązania Matijna i DxinX dotyczą zbiorów danych, które nie zapewniają dostępu swobodnego. W przypadku zestawów danych, które to robią (a SQL robi to OFFSET), jest to niepotrzebnie nieefektywne.
Endre Both
@EndreOba rzeczywiście. Po prostu podoba mi się „wydajność” kodowania polegająca na stosowaniu tego samego podejścia niezależnie od źródła danych. Czasami wydajność próbkowania danych nie wpływa znacząco na wydajność potoku ograniczonego przez inne procesy (cokolwiek faktycznie robisz z danymi, na przykład szkolenie ML).
płyty kuchenne
1

O wiele łatwiejsze podejście polega po prostu na przefiltrowaniu do interesującego nas zestawu rekordów i użyciu random.sampledo wybrania tylu, ile chcesz:

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

Zauważ, że powinieneś mieć na miejscu kod, aby sprawdzić, czy my_querysetnie jest pusty; random.samplezwraca, ValueError: sample larger than populationjeśli pierwszy argument zawiera zbyt mało elementów.

eykanal
źródło
2
Czy spowoduje to pobranie całego zestawu zapytań?
perrohunter
@perrohunter To nawet nie będzie działać Queryset(przynajmniej z Pythonem 3.7 i Django 2.1); musisz najpierw przekonwertować go na listę, która oczywiście pobiera cały zestaw zapytań.
Endre Both
@EndreBoth - to zostało napisane w 2016 roku, kiedy żadne z nich nie istniało.
eykanal
Dlatego dodałem informacje o wersji. Ale jeśli zadziałało w 2016 roku, zrobił to, przeciągając cały zestaw zapytań do listy, prawda?
Endre Both
@EndreBoth Poprawne.
eykanal
1

Cześć, potrzebowałem wybrać losowy rekord z zestawu zapytań, którego długość również musiałem zgłosić (tj. Strona internetowa wyprodukowała opisaną pozycję i wspomniane rekordy pozostały)

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

trwało o połowę krócej (0,7 s vs 1,7 s) niż:

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

Domyślam się, że pozwala to uniknąć ściągnięcia całego zapytania przed wybraniem losowego wpisu i sprawiło, że mój system był wystarczająco responsywny, aby strona była wielokrotnie odwiedzana w celu wykonania powtarzalnego zadania, w którym użytkownicy chcą zobaczyć odliczanie item_count.

pjmnoble
źródło
0

Metoda automatycznego zwiększania wartości klucza podstawowego bez usuwania

Jeśli masz tabelę, w której klucz podstawowy jest sekwencyjną liczbą całkowitą bez przerw, następująca metoda powinna działać:

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

Ta metoda jest znacznie wydajniejsza niż inne metody, które wykonują iterację we wszystkich wierszach tabeli. Chociaż wymaga dwóch zapytań do bazy danych, oba są trywialne. Ponadto jest to proste i nie wymaga definiowania żadnych dodatkowych klas. Jednak jego zastosowanie jest ograniczone do tabel z automatycznie zwiększającym się kluczem podstawowym, w których wiersze nigdy nie zostały usunięte, tak że nie ma przerw w sekwencji identyfikatorów.

W przypadku, gdy wiersze zostały usunięte, takie jak luki, ta metoda może nadal działać, jeśli zostanie ponowiona, dopóki istniejący klucz podstawowy nie zostanie losowo wybrany.

Bibliografia

Daniel Himmelstein
źródło
0

Mam bardzo proste rozwiązanie, wykonaj custom managera:

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

a następnie dodaj model:

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

Teraz możesz go użyć:

Example.objects.random()
LagRange
źródło
z losowego wyboru importu
Adam Starrh
3
Nie używaj tej metody, jeśli chcesz szybkości. To rozwiązanie jest BARDZO powolne. Sprawdziłem. Wolniej niż order_by('?').first()ponad 60 razy.
LagRange
@ Alex78191 nie, "?" jest też zła, ale moja metoda jest BARDZO powolna. Użyłem najlepszego rozwiązania odpowiedzi.
LagRange