Wie kann man mit Djangos ORM einen zufälligen Datensatz ziehen?

176

Ich habe ein Modell, das Bilder darstellt, die ich auf meiner Website präsentiere. Auf der Hauptwebseite möchte ich einige davon zeigen: die neueste, eine, die die meiste Zeit nicht besucht wurde, die beliebteste und eine zufällige.

Ich benutze Django 1.0.2.

Während die ersten drei mit Django-Modellen leicht zu ziehen sind, bereitet mir die letzte (zufällige) einige Probleme. Ich kann es aus meiner Sicht so codieren:

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

Es sieht nicht nach etwas aus, das ich aus meiner Sicht gerne hätte - dies ist vollständig Teil der Datenbankabstraktion und sollte im Modell enthalten sein. Außerdem muss ich mich hier um entfernte Datensätze kümmern (dann deckt die Anzahl aller Datensätze nicht alle möglichen Schlüsselwerte ab) und wahrscheinlich um viele andere Dinge.

Gibt es noch andere Möglichkeiten, wie ich das machen kann, vorzugsweise irgendwie innerhalb der Modellabstraktion?

Kender
quelle
Wie Sie Dinge anzeigen und welche Dinge Sie anzeigen, ist meiner Meinung nach Teil der Ebene "Ansicht" oder der Geschäftslogik, die in der Ebene "Controller" von MVC enthalten sein sollte.
Gabriele D'Antona
In Django ist der Controller die Ansicht. docs.djangoproject.com/de/dev/faq/general/…

Antworten:

169

Mit order_by('?')wird der Datenbankserver am zweiten Tag in der Produktion beendet. Ein besserer Weg ist so etwas wie das Abrufen einer zufälligen Zeile aus einer relationalen Datenbank .

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
quelle
45
Was sind die Vorteile von model.objects.aggregate(count=Count('id'))['count']übermodel.objects.all().count()
Ryan Saxe
11
Beachten Sie, dass dieser Ansatz zwar viel besser als die akzeptierte Antwort ist, jedoch zwei SQL-Abfragen durchführt. Wenn sich die Anzahl zwischendurch ändert, kann möglicherweise ein Fehler außerhalb der Grenzen auftreten.
Nelo Mitranim
2
Dies ist eine falsche Lösung. Es funktioniert nicht, wenn Ihre IDs nicht bei 0 beginnen. Und auch, wenn die IDs nicht zusammenhängend sind. Angenommen, der erste Datensatz beginnt bei 500 und der letzte bei 599 (unter der Annahme von Kontiguität). Dann würde die Anzahl 54950 betragen. Sicherlich existiert die Liste [54950] nicht, da die Länge Ihres Abfragers 100 beträgt. Dadurch wird der Index aus der gebundenen Ausnahme geworfen. Ich weiß nicht, warum so viele Leute dies befürworteten und dies als akzeptierte Antwort markiert wurde.
Sajid
1
@sajid: Warum genau fragst du mich? Es ist ziemlich einfach, die Gesamtsumme meiner Beiträge zu dieser Frage zu sehen: Bearbeiten eines Links, um auf ein Archiv zu verweisen, nachdem es verfault ist. Ich habe noch nicht einmal über eine der Antworten abgestimmt. Aber ich finde es amüsant, dass diese Antwort und die, von der Sie behaupten, dass sie viel besser sind, beide tatsächlich verwendet werden .all()[randint(0, count - 1)]. Vielleicht sollten Sie sich darauf konzentrieren, herauszufinden, welcher Teil der Antwort falsch oder schwach ist, anstatt "Off-by-One-Error" für uns neu zu definieren und die dummen Wähler anzuschreien. (Vielleicht ist es, dass es nicht verwendet .objects?)
Nathan Tuggy
3
@ NathanTuggy. Ok mein schlechtes. Entschuldigung
Sajid
259

Verwenden Sie einfach:

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

Es ist in der QuerySet-API dokumentiert .

Muhuk
quelle
71
Bitte beachten Sie, dass dieser Ansatz sehr langsam sein kann, wie dokumentiert :)
Nicolas Dumazet
6
"Je nach verwendetem Datenbank-Backend kann es teuer und langsam sein." - Erfahrungen mit verschiedenen DB-Backends? (sqlite / mysql / postgres)?
Kender
4
Ich habe es nicht getestet, daher ist dies reine Spekulation: Warum sollte es langsamer sein, als alle Elemente abzurufen und in Python eine Randomisierung durchzuführen?
Muhuk
8
Ich habe gelesen, dass es in MySQL langsam ist, da MySQL eine unglaublich ineffiziente zufällige Reihenfolge hat.
Brandon Henry
33
Warum nicht einfach random.choice(Model.objects.all())?
Jamey
25

Die Lösungen mit order_by ('?') [: N] sind selbst für mittelgroße Tabellen extrem langsam, wenn Sie MySQL verwenden (Sie kennen keine anderen Datenbanken).

order_by('?')[:N]wird in eine SELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT NAbfrage übersetzt .

Dies bedeutet, dass für jede Zeile in der Tabelle die Funktion RAND () ausgeführt wird, dann die gesamte Tabelle nach dem Wert dieser Funktion sortiert wird und dann zuerst N Datensätze zurückgegeben werden. Wenn Ihre Tische klein sind, ist dies in Ordnung. In den meisten Fällen ist dies jedoch eine sehr langsame Abfrage.

Ich habe eine einfache Funktion geschrieben, die auch dann funktioniert, wenn IDs Löcher haben (einige Zeilen wurden gelöscht):

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]

Es ist in fast allen Fällen schneller als order_by ('?').

Mikhail Korobov
quelle
30
Leider ist es auch alles andere als zufällig. Wenn Sie einen Datensatz mit der ID 1 und einen anderen mit der ID 100 haben, wird der zweite in 99% der Fälle zurückgegeben.
DS.
16

Hier ist eine einfache Lösung:

from random import randint

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

Sie können einen Manager für Ihr Modell erstellen, um solche Aufgaben auszuführen. Um zuerst zu verstehen , was ein Manager ist, das Painting.objectsist ein Methode Manager, enthält all(), filter(), get()etc. Ihren eigenen Manager erstellen , können Sie die Ergebnisse Filter vor und alle die gleichen Methoden haben, sowie Ihre eigenen Methoden, die Arbeit an den Ergebnissen .

BEARBEITEN : Ich habe meinen Code geändert, um die order_by['?']Methode widerzuspiegeln . Beachten Sie, dass der Manager eine unbegrenzte Anzahl von Zufallsmodellen zurückgibt. Aus diesem Grund habe ich ein wenig Verwendungscode eingefügt, um zu zeigen, wie man nur ein einziges Modell erhält.

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.

Verwendung

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

Schließlich können Sie viele Manager in Ihren Modellen haben. Sie können also ein LeastViewsManager()oder erstellen MostPopularManager().

Soviut
quelle
3
Die Verwendung von get () funktioniert nur, wenn Ihre pks aufeinanderfolgend sind, dh Sie löschen niemals Elemente. Andernfalls versuchen Sie wahrscheinlich, ein Paket zu erhalten, das nicht vorhanden ist. Die Verwendung von .all () [random_index] leidet nicht unter diesem Problem und ist auch nicht weniger effizient.
Daniel Roseman
Ich habe das verstanden, weshalb mein Beispiel den Code der Frage einfach mit einem Manager repliziert. Es wird weiterhin Sache des OP sein, seine Grenzen zu überprüfen.
Soviut
1
Anstatt .get (id = random_index) zu verwenden, wäre es nicht besser, .filter (id__gte = random_index) [0: 1] zu verwenden? Erstens hilft es, das Problem mit nicht aufeinanderfolgenden pks zu lösen. Zweitens sollte get_query_set ... ein QuerySet zurückgeben. Und in Ihrem Beispiel nicht.
Nicolas Dumazet
2
Ich würde keinen neuen Manager erstellen, nur um eine Methode unterzubringen. Ich würde dem Standardmanager "get_random" hinzufügen, damit Sie nicht jedes Mal, wenn Sie das zufällige Bild benötigen, den all () [0] -Beifen durchlaufen müssen. Wenn der Autor ein ForeignKey für ein Benutzermodell wäre, könnten Sie außerdem user.painting_set.get_random () sagen.
Antti Rasinen
Normalerweise erstelle ich einen neuen Manager, wenn ich eine umfassende Aktion ausführen möchte, z. B. eine Liste mit zufälligen Datensätzen. Ich würde eine Methode auf dem Standardmanager erstellen, wenn ich eine spezifischere Aufgabe mit den Datensätzen ausführen würde, die ich bereits hatte.
Soviut
6

Die anderen Antworten sind entweder potenziell langsam (verwenden order_by('?')) oder verwenden mehr als eine SQL-Abfrage. Hier ist eine Beispiellösung ohne Bestellung und mit nur einer Abfrage (unter der Annahme von Postgres):

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

Beachten Sie, dass dies einen Indexfehler auslöst, wenn die Tabelle leer ist. Schreiben Sie sich eine modellunabhängige Hilfsfunktion, um dies zu überprüfen.

Nelo Mitranim
quelle
Ein guter Proof of Concept, aber dies sind auch zwei Abfragen in der Datenbank. Sie speichern einen Roundtrip zur Datenbank. Sie müssten dies sehr oft ausführen, damit sich das Schreiben und Verwalten einer Rohabfrage lohnt. Und wenn Sie sich vor leeren Tabellen schützen möchten, können Sie eine count()im Voraus ausführen und auf die Rohabfrage verzichten.
Endre Both
2

Nur eine einfache Idee, wie ich es mache:

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

Nur um einen (ziemlich häufigen) Sonderfall zu beachten: Wenn die Tabelle eine indizierte Spalte für das automatische Inkrementieren ohne Löschvorgänge enthält, ist eine Abfrage wie die optimale Methode für eine zufällige Auswahl:

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

das setzt eine solche Spalte mit dem Namen id für Tabelle voraus. In Django können Sie dies tun durch:

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

in dem Sie den Anwendungsnamen durch Ihren Anwendungsnamen ersetzen müssen.

Im Allgemeinen kann mit einer ID-Spalte order_by ('?') Viel schneller ausgeführt werden mit:

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

Dies wird dringend empfohlen. Es wird eine zufällige Zeile aus einer relationalen Datenbank abgerufen

Weil die Verwendung von django orm, um so etwas zu tun, Ihren Datenbankserver besonders dann verärgert, wenn Sie eine große Datentabelle haben: |

Und die Lösung besteht darin, einen Modellmanager bereitzustellen und die SQL-Abfrage von Hand zu schreiben;)

Update :

Eine andere Lösung, die auf jedem Datenbank-Backend funktioniert, auch auf nicht-rel-Lösungen, ohne benutzerdefinierte Informationen zu schreiben ModelManager. Zufällige Objekte aus einem Queryset in Django abrufen

Alireza Savand
quelle
1

Möglicherweise möchten Sie denselben Ansatz verwenden , mit dem Sie einen Iterator testen, insbesondere wenn Sie mehrere Elemente testen möchten, um einen Beispielsatz zu erstellen . @MatijnPieters und @DzinX haben viel darüber nachgedacht:

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
Kochfelder
quelle
Die Lösung von Matijn und DxinX besteht für Datensätze, die keinen wahlfreien Zugriff bieten. Für Datensätze, die dies tun (und SQL tut dies OFFSET), ist dies unnötig ineffizient.
Endre Both
@ EndreBoth in der Tat. Ich mag nur die Codierung "Effizienz" der Verwendung des gleichen Ansatzes unabhängig von der Datenquelle. Manchmal wirkt sich die Effizienz der Datenabtastung nicht wesentlich auf die Leistung einer Pipeline aus, die durch andere Prozesse eingeschränkt ist (was auch immer Sie tatsächlich mit den Daten tun, wie z. B. ML-Training).
Kochfelder
1

Ein viel einfacherer Ansatz besteht darin, einfach nach dem gewünschten Datensatz zu filtern und random.sampleso viele auszuwählen, wie Sie möchten:

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

Beachten Sie, dass Sie über Code verfügen sollten, um sicherzustellen, dass dieser my_querysetnicht leer ist. random.sampleGibt zurück, ValueError: sample larger than populationwenn das erste Argument zu wenige Elemente enthält.

Eykanal
quelle
2
Wird dadurch der gesamte Abfragesatz abgerufen?
Perrohunter
@perrohunter Es wird nicht einmal funktionieren Queryset(zumindest mit Python 3.7 und Django 2.1); Sie müssen es zuerst in eine Liste konvertieren, die offensichtlich den gesamten Abfragesatz abruft.
Endre Both
@EndreBoth - Dies wurde 2016 geschrieben, als keines davon existierte.
Eykanal
Deshalb habe ich die Versionsinformationen hinzugefügt. Aber wenn es 2016 funktioniert hat, hat es das gesamte Abfrageset in eine Liste gezogen, oder?
Endre Both
@ EndreBoth Richtig.
Eykanal
1

Hallo, ich musste einen zufälligen Datensatz aus einem Abfragesatz auswählen, dessen Länge ich auch melden musste (dh die Webseite produzierte das beschriebene Element und die Datensätze blieben übrig).

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

dauerte halb so lange (0,7s vs 1,7s) wie:

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

Ich vermute, es vermeidet, die gesamte Abfrage herunterzuziehen, bevor der zufällige Eintrag ausgewählt wird, und mein System reagiert auf eine Seite, auf die wiederholt zugegriffen wird, für eine sich wiederholende Aufgabe, bei der Benutzer den Countdown item_count sehen möchten.

pjmnoble
quelle
0

Methode zum automatischen Inkrementieren des Primärschlüssels ohne Löschen

Wenn Sie eine Tabelle haben, in der der Primärschlüssel eine sequentielle Ganzzahl ohne Lücken ist, sollte die folgende Methode funktionieren:

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

Diese Methode ist viel effizienter als andere Methoden, die alle Zeilen der Tabelle durchlaufen. Es sind zwar zwei Datenbankabfragen erforderlich, beide sind jedoch trivial. Darüber hinaus ist es einfach und erfordert keine Definition zusätzlicher Klassen. Die Anwendbarkeit ist jedoch auf Tabellen mit einem automatisch inkrementierenden Primärschlüssel beschränkt, bei denen Zeilen nie gelöscht wurden, sodass keine Lücken in der Reihenfolge der IDs bestehen.

In dem Fall, in dem Zeilen so gelöscht wurden, dass es sich um Lücken handelt, kann diese Methode weiterhin funktionieren, wenn sie wiederholt wird, bis ein vorhandener Primärschlüssel zufällig ausgewählt wird.

Verweise

Daniel Himmelstein
quelle
0

Ich habe eine sehr einfache Lösung, machen Sie einen benutzerdefinierten Manager:

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

und dann im Modell hinzufügen:

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

Jetzt können Sie es verwenden:

Example.objects.random()
LagRange
quelle
aus zufälliger Importauswahl
Adam Starrh
3
Bitte verwenden Sie diese Methode nicht, wenn Sie Geschwindigkeit wünschen. Diese Lösung ist sehr langsam. Ich habe nachgesehen. Es ist langsamer als order_by('?').first()mehr als 60 Mal.
LagRange
@ Alex78191 nein, "?" ist auch schlecht, aber meine Methode ist EXTRA langsam. Ich habe eine Top-Antwort-Lösung verwendet.
LagRange