Come ottenere un record casuale usando l'ORM di Django?


176

Ho un modello che rappresenta i dipinti che presento sul mio sito. Sulla pagina web principale vorrei mostrarne alcuni: il più recente, uno che non è stato visitato per la maggior parte del tempo, il più popolare e uno a caso.

Sto usando Django 1.0.2.

Mentre i primi 3 sono facili da usare con i modelli django, l'ultimo (casuale) mi dà qualche problema. Posso ofc codificarlo a mio avviso, a qualcosa del genere:

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

Non sembra qualcosa che mi piacerebbe avere a mio avviso, questo è interamente parte dell'astrazione del database e dovrebbe essere nel modello. Inoltre, qui devo occuparmi dei record rimossi (quindi il numero di tutti i record non mi coprirà tutti i possibili valori chiave) e probabilmente molte altre cose.

Altre opzioni su come posso farlo, preferibilmente in qualche modo all'interno dell'astrazione del modello?


A mio avviso, il modo in cui visualizzi le cose e quali elementi fai parte del livello "Visualizza" o della logica aziendale che dovrebbe andare nel livello "Controller" di MVC.
Gabriele D'Antona,

In Django il controller è la vista. docs.djangoproject.com/en/dev/faq/general/…

Risposte:


169

L'utilizzo order_by('?')ucciderà il server db il secondo giorno di produzione. Un modo migliore è qualcosa di simile a quello descritto in Ottenere una riga casuale da un database relazionale .

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
Quali sono i vantaggi di model.objects.aggregate(count=Count('id'))['count']overmodel.objects.all().count()
Ryan Saxe,

11
Sebbene molto meglio della risposta accettata, nota che questo approccio fa due query SQL. Se il conteggio cambia tra, potrebbe essere possibile ottenere un errore fuori limite.
Nelo Mitranim,

2
Questa è una soluzione sbagliata Non funzionerà se i tuoi ID non iniziano da 0. E anche quando gli ID non sono contigui. Ad esempio, il primo record inizia da 500 e l'ultimo è 599 (presupponendo contiguità). Quindi il conteggio sarebbe 54950. L'elenco [54950] sicuramente non esiste perché la lunghezza della query è 100. Verrà generato l'indice dall'eccezione associata. Non so perché così tante persone abbiano votato a favore e questo è stato contrassegnato come risposta accettata.
sajid,

1
@sajid: Perché, esattamente, me lo chiedi? È abbastanza facile vedere la somma totale dei miei contributi a questa domanda: modificare un link per puntare a un archivio dopo che è marcito. Non ho nemmeno votato nessuna delle risposte. Ma trovo divertente che questa risposta e quella che tu affermi di essere molto meglio usino entrambe .all()[randint(0, count - 1)]in effetti. Forse dovresti concentrarti sull'individuazione di quale parte della risposta è sbagliata o debole, piuttosto che ridefinire "errore per singolo" per noi e urlare contro gli elettori sciocchi. (Forse è che non sta usando .objects?)
Nathan Tuggy il

3
@NathanTuggy. Ok mio male. Siamo spiacenti
sajid il

260

Usa semplicemente:

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

È documentato nell'API QuerySet .


71
Si noti che questo approccio può essere molto lento, come documentato :)
Nicolas Dumazet,

6
"può essere costoso e lento, a seconda del backend del database che stai utilizzando." - qualche esperienza su back-end DB diversi? (SQLite / mysql / postgres)?
kender,

4
Non l'ho testato, quindi questa è pura speculazione: perché dovrebbe essere più lento del recupero di tutti gli elementi e l'esecuzione della randomizzazione in Python?
Muhuk,

8
ho letto che è lento in mysql, poiché mysql ha un ordinamento casuale incredibilmente inefficiente.
Brandon Henry,

33
Perché non solo random.choice(Model.objects.all())?
Jamey,

25

Le soluzioni con order_by ('?') [: N] sono estremamente lente anche per le tabelle di medie dimensioni se usi MySQL (non conosci altri database).

order_by('?')[:N]sarà tradotto in SELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT Nquery.

Significa che per ogni riga nella tabella verrà eseguita la funzione RAND (), quindi l'intera tabella verrà ordinata in base al valore di questa funzione e quindi verranno restituiti i primi N record. Se i tuoi tavoli sono piccoli, va bene. Ma nella maggior parte dei casi si tratta di una query molto lenta.

Ho scritto una funzione semplice che funziona anche se gli ID hanno buchi (alcune righe sono state cancellate):

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]

È più veloce di order_by ('?') In quasi tutti i casi.


30
Inoltre, purtroppo, è tutt'altro che casuale. Se hai un record con ID 1 e un altro con ID 100, restituirà il secondo il 99% delle volte.
DS.

16

Ecco una soluzione semplice:

from random import randint

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

10

Potresti creare un manager sul tuo modello per fare questo genere di cose. Per capire prima che cosa è un manager, il Painting.objectsmetodo è un manager che contiene all(), filter(),get() , ecc Creare il proprio Manager consente di pre-filtro risultati e hanno tutti gli stessi metodi, così come i propri metodi personalizzati, il lavoro sui risultati .

EDIT : ho modificato il mio codice per riflettere il order_by['?']metodo. Si noti che il gestore restituisce un numero illimitato di modelli casuali. Per questo motivo ho incluso un po 'di codice di utilizzo per mostrare come ottenere un solo modello.

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.

uso

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

Infine, puoi avere molti manager sui tuoi modelli, quindi sentiti libero di creare un LeastViewsManager()o MostPopularManager().


3
L'uso di get () funzionerebbe solo se i tuoi pk sono consecutivi, ovvero non elimini mai alcun elemento. Altrimenti è probabile che tu provi a ottenere un pk che non esiste. L'uso di .all () [random_index] non soffre di questo problema e non è meno efficiente.
Daniel Roseman,

Ho capito che è per questo che il mio esempio replica semplicemente il codice della domanda con un manager. Spetterà comunque all'OP elaborare i suoi limiti di controllo.
Soviet

1
invece di usare .get (id = random_index) non sarebbe meglio usare .filter (id__gte = random_index) [0: 1]? In primo luogo, aiuta a risolvere il problema con pks non consecutivi. Secondo, get_query_set dovrebbe restituire ... un QuerySet. E nel tuo esempio no.
Nicolas Dumazet,

2
Non vorrei creare un nuovo manager solo per ospitare un metodo. Aggiungerei "get_random" al gestore predefinito in modo da non dover passare attraverso il telaio all () [0] ogni volta che hai bisogno dell'immagine casuale. Inoltre, se l'autore fosse un ForeignKey per un modello utente, si potrebbe dire user.painting_set.get_random ().
Antti Rasinen,

In genere creo un nuovo manager quando voglio un'azione generale, come ottenere un elenco di record casuali. Creerei un metodo sul gestore predefinito se stavo facendo un'attività più specifica con i record che avevo già.
Soviet

6

Le altre risposte sono potenzialmente lente (utilizzando order_by('?')) o utilizzano più di una query SQL. Ecco una soluzione di esempio senza ordini e solo una query (presupponendo Postgres):

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

Tenere presente che ciò genererà un errore di indice se la tabella è vuota. Scrivi a te stesso una funzione di supporto indipendente dal modello per verificarlo.


Una bella dimostrazione di concetto, ma si tratta di due query anche all'interno del database, ciò che si salva è un roundtrip al database. Dovresti eseguire questo molte volte per rendere la scrittura e il mantenimento di una query non valida. E se si desidera proteggersi da tabelle vuote, è possibile eseguire un count()anticipo in anticipo e rinunciare alla query non elaborata.
Endre Both,

2

Solo una semplice idea di come lo faccio:

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

1

Solo per notare un caso speciale (abbastanza comune), se nella tabella è presente una colonna di incremento automatico indicizzata senza eliminazioni, il modo ottimale per effettuare una selezione casuale è una query come:

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

che assume tale colonna denominata id per table. In django puoi farlo tramite:

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

in cui è necessario sostituire il nome app con il nome dell'applicazione.

In generale, con una colonna id, order_by ('?') Può essere eseguito molto più velocemente con:

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

1

Questo è altamente raccomandato Ottenere una riga casuale da un database relazionale

Perché usare django orm per fare una cosa del genere, farà arrabbiare il tuo server db specialmente se hai una tabella di big data: |

E la soluzione è fornire un Model Manager e scrivere manualmente la query SQL;)

Aggiornare :

Un'altra soluzione che funziona su qualsiasi back-end di database anche su quelli non rel senza scrivere personalizzati ModelManager. Ottenere oggetti casuali da un Queryset in Django


1

Potresti voler utilizzare lo stesso approccio che utilizzeresti per campionare qualsiasi iteratore, specialmente se prevedi di campionare più elementi per creare un set di campioni . @MatijnPieters e @DzinX ci hanno pensato molto:

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

La soluzione di Matijn e DxinX è per set di dati che non forniscono accesso casuale. Per i set di dati che lo fanno (e lo fa SQL OFFSET), ciò è inutilmente inefficiente.
Endre Both,

@EndreBoth davvero. Mi piace la "efficienza" di codifica dell'utilizzo dello stesso approccio indipendentemente dall'origine dati. A volte l'efficienza del campionamento dei dati non influisce in modo significativo sulle prestazioni di una pipeline limitata da altri processi (qualunque cosa tu stia effettivamente facendo con i dati, come l'addestramento ML).
Piani cottura

1

Un approccio molto più semplice a questo comporta semplicemente il filtraggio fino al recordset di interesse e l'utilizzo random.sampleper selezionare il numero desiderato:

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

Si noti che è necessario disporre di un codice per verificare che my_querysetnon sia vuoto; random.sampleritorna ValueError: sample larger than populationse il primo argomento contiene troppi elementi.


2
Questo causerà il recupero dell'intero set di query?
perrohunter

@perrohunter Non funzionerà nemmeno con Queryset(almeno con Python 3.7 e Django 2.1); devi prima convertirlo in un elenco, che ovviamente recupera l'intero queryset.
Endre Both,

@EndreBoth - questo è stato scritto nel 2016, quando nessuno dei due esisteva.
eykanal,

Ecco perché ho aggiunto le informazioni sulla versione. Ma se ha funzionato nel 2016, lo ha fatto inserendo l'intero queryset in un elenco, giusto?
Endre Both,

@EndreBoth Correct.
eykanal,

1

Ciao, dovevo selezionare un record casuale da un queryset di cui avrei dovuto riferire anche la lunghezza (ovvero la pagina web ha prodotto l'elemento descritto e ha detto che i record sono rimasti)

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

ha impiegato la metà (0,7 secondi contro 1,7 secondi) di:

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

Immagino che eviti di tirare giù l'intera query prima di selezionare la voce casuale e abbia reso il mio sistema abbastanza reattivo per una pagina a cui si accede ripetutamente per un'attività ripetitiva in cui gli utenti vogliono vedere il conto alla rovescia di item_count.


0

Metodo per l'incremento automatico della chiave primaria senza eliminazioni

Se hai una tabella in cui la chiave primaria è un numero intero sequenziale senza spazi vuoti, dovrebbe funzionare il seguente metodo:

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

Questo metodo è molto più efficiente di altri metodi qui che ripetono tutte le righe della tabella. Mentre richiede due query sul database, entrambe sono banali. Inoltre, è semplice e non richiede la definizione di classi extra. Tuttavia, l'applicabilità è limitata alle tabelle con una chiave primaria a incremento automatico in cui le righe non sono mai state eliminate, in modo tale che non vi siano spazi vuoti nella sequenza di ID.

Nel caso in cui le righe siano state eliminate in modo tale da essere vuoti, questo metodo potrebbe comunque funzionare se viene riprovato fino a quando una chiave primaria esistente viene selezionata casualmente.

Riferimenti


0

Ho una soluzione molto semplice, crea un gestore personalizzato:

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

e quindi aggiungere nel modello:

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

Ora puoi usarlo:

Example.objects.random()

dalla scelta di importazione casuale
Adam Starrh,

3
Per favore, non usare questo metodo, se vuoi la velocità. Questa soluzione è MOLTO lenta. Ho controllato. È più lento di order_by('?').first()più di 60 volte.
LagRange

@ Alex78191 no, "?" è anche un male, ma il mio metodo è EXTRA lento. Ho usato la soluzione di risposta migliore.
LagRange
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.