Comment extraire un enregistrement aléatoire en utilisant l'ORM de Django?


176

J'ai un modèle qui représente des peintures que je présente sur mon site. Sur la page Web principale, j'aimerais en montrer quelques-uns: le plus récent, celui qui n'a pas été visité la plupart du temps, le plus populaire et un aléatoire.

J'utilise Django 1.0.2.

Alors que les 3 premiers d'entre eux sont faciles à tirer en utilisant les modèles django, le dernier (aléatoire) me cause des problèmes. Je peux ofc le coder à mon avis, à quelque chose comme ceci:

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

Cela ne ressemble pas à quelque chose que j'aimerais avoir à mon avis - cela fait entièrement partie de l'abstraction de la base de données et devrait être dans le modèle. De plus, ici, je dois m'occuper des enregistrements supprimés (alors le nombre de tous les enregistrements ne me couvrira pas toutes les valeurs clés possibles) et probablement beaucoup d'autres choses.

D'autres options comment je peux le faire, de préférence d'une manière ou d'une autre à l'intérieur de l'abstraction du modèle?


La façon dont vous affichez les choses et les choses que vous affichez fait partie du niveau "Affichage" ou de la logique métier qui devrait, à mon avis, entrer dans le niveau "Contrôleur" de MVC.
Gabriele D'Antona

Dans Django, le contrôleur est la vue. docs.djangoproject.com/en/dev/faq/general/…

Réponses:


169

L'utilisation order_by('?')tuera le serveur db le deuxième jour de production. Un meilleur moyen est quelque chose comme ce qui est décrit dans Obtenir une ligne aléatoire à partir d'une base de données relationnelle .

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
Quels sont les avantages de model.objects.aggregate(count=Count('id'))['count']overmodel.objects.all().count()
Ryan Saxe

11
Bien que bien meilleure que la réponse acceptée, notez que cette approche effectue deux requêtes SQL. Si le nombre change entre les deux, il peut être possible d'obtenir une erreur hors limites.
Nelo Mitranim

2
C'est une mauvaise solution. Cela ne fonctionnera pas si vos identifiants ne partent pas de 0. Et aussi quand les identifiants ne sont pas contigus. Disons que le premier enregistrement commence à 500 et le dernier à 599 (en supposant la contiguïté). Alors le nombre serait 54950. La liste [54950] n'existe sûrement pas car la longueur de votre système de recherche est de 100. Elle jettera l'index hors de l'exception liée. Je ne sais pas pourquoi tant de personnes ont voté pour cela et cela a été marqué comme une réponse acceptée.
sajid le

1
@sajid: Pourquoi, exactement, me demandez-vous? Il est assez facile de voir la somme totale de mes contributions à cette question: éditer un lien pour pointer vers une archive après qu'elle a pourri. Je n'ai même voté sur aucune des réponses. Mais je trouve amusant que cette réponse et celle que vous prétendez être bien meilleure utilisent toutes les deux .all()[randint(0, count - 1)]en effet. Peut-être devriez-vous vous concentrer sur l'identification de la partie de la réponse qui est fausse ou faible, plutôt que de redéfinir «l'erreur par erreur» pour nous et de crier après les électeurs insensés. (C'est peut-être qu'il n'utilise pas .objects?)
Nathan Tuggy

3
@NathanTuggy. Ok mon mal. Désolé
sajid le

260

Utilisez simplement:

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

Il est documenté dans l' API QuerySet .


71
Veuillez noter que cette approche peut être très lente, comme documenté :)
Nicolas Dumazet

6
«peut être coûteux et lent, selon le backend de base de données que vous utilisez. - une expérience sur différents backends DB? (sqlite / mysql / postgres)?
kender

4
Je ne l'ai pas testé, c'est donc une pure spéculation: pourquoi devrait-il être plus lent que de récupérer tous les éléments et d'effectuer une randomisation en Python?
muhuk

8
J'ai lu que c'est lent dans mysql, car mysql a un ordre aléatoire incroyablement inefficace.
Brandon Henry

33
Pourquoi pas juste random.choice(Model.objects.all())?
Jamey

25

Les solutions avec order_by ('?') [: N] sont extrêmement lentes même pour les tables de taille moyenne si vous utilisez MySQL (ne connaissez pas les autres bases de données).

order_by('?')[:N] sera traduit en SELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT N requête.

Cela signifie que pour chaque ligne de la table, la fonction RAND () sera exécutée, puis la table entière sera triée en fonction de la valeur de cette fonction, puis les N premiers enregistrements seront renvoyés. Si vos tables sont petites, c'est très bien. Mais dans la plupart des cas, il s'agit d'une requête très lente.

J'ai écrit une fonction simple qui fonctionne même si les identifiants ont des trous (certaines lignes ont été supprimées):

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]

C'est plus rapide que order_by ('?') Dans presque tous les cas.


30
Aussi, malheureusement, c'est loin d'être aléatoire. Si vous avez un enregistrement avec l'id 1 et un autre avec l'id 100, il renverra le second 99% du temps.
DS.

16

Voici une solution simple:

from random import randint

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

10

Vous pouvez créer un gestionnaire sur votre modèle pour faire ce genre de chose. D'abord comprendre ce qu'est un gestionnaire est, la Painting.objectsméthode est un gestionnaire qui contient all(), filter(), get(), etc. Créer votre propre gestionnaire vous permet d' effectuer une pré-filtrage des résultats et ont tous les mêmes méthodes, ainsi que vos propres méthodes personnalisées, le travail sur les résultats .

EDIT : j'ai modifié mon code pour refléter la order_by['?']méthode. Notez que le gestionnaire renvoie un nombre illimité de modèles aléatoires. Pour cette raison, j'ai inclus un peu de code d'utilisation pour montrer comment obtenir un seul modèle.

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.

Usage

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

Enfin, vous pouvez avoir de nombreux managers sur vos modèles, alors n'hésitez pas à créer un LeastViewsManager()ou MostPopularManager().


3
Utiliser get () ne fonctionnerait que si vos pks sont consécutifs, c'est-à-dire que vous ne supprimez jamais aucun élément. Sinon, vous risquez d'essayer d'obtenir un pk qui n'existe pas. Utiliser .all () [random_index] ne souffre pas de ce problème et n'est pas moins efficace.
Daniel Roseman

J'ai compris cela, c'est pourquoi mon exemple reproduit simplement le code de la question avec un gestionnaire. Il appartiendra toujours à l'OP de déterminer sa vérification des limites.
Soviut

1
au lieu d'utiliser .get (id = random_index) ne serait-il pas préférable d'utiliser .filter (id__gte = random_index) [0: 1]? Premièrement, cela aide à résoudre le problème avec des pks non consécutifs. Deuxièmement, get_query_set doit renvoyer ... un QuerySet. Et dans votre exemple, ce n'est pas le cas.
Nicolas Dumazet

2
Je ne créerais pas un nouveau manager juste pour héberger une méthode. J'ajouterais "get_random" au gestionnaire par défaut pour que vous n'ayez pas à passer par le cerceau all () [0] chaque fois que vous avez besoin de l'image aléatoire. De plus, si author était une ForeignKey pour un modèle User, vous pourriez dire user.painting_set.get_random ().
Antti Rasinen

Je crée généralement un nouveau gestionnaire lorsque je veux une action globale, comme obtenir une liste d'enregistrements aléatoires. Je créerais une méthode sur le gestionnaire par défaut si je faisais une tâche plus spécifique avec les enregistrements que j'avais déjà.
Soviut

6

Les autres réponses sont potentiellement lentes (utilisation order_by('?')) ou utilisent plus d'une requête SQL. Voici un exemple de solution sans ordre et avec une seule requête (en supposant Postgres):

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

Sachez que cela provoquera une erreur d'index si la table est vide. Écrivez-vous une fonction d'assistance indépendante du modèle pour vérifier cela.


Une belle preuve de concept, mais il s'agit également de deux requêtes à l'intérieur de la base de données, ce que vous enregistrez est un aller-retour vers la base de données. Vous devrez l'exécuter plusieurs fois pour que l'écriture et la maintenance d'une requête brute en valent la peine. Et si vous voulez vous prémunir contre les tables vides, vous pouvez tout aussi bien exécuter une count()requête à l'avance et vous passer de la requête brute.
Endre Both

2

Juste une idée simple comment je le fais:

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

1

Juste pour noter un cas particulier (assez courant), s'il y a une colonne auto-incrémentée indexée dans la table sans suppression, la façon optimale de faire une sélection aléatoire est une requête comme:

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

qui suppose une telle colonne nommée id pour table. Dans django, vous pouvez le faire en:

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

dans lequel vous devez remplacer appname par le nom de votre application.

En général, avec une colonne id, le order_by ('?') Peut être fait beaucoup plus rapidement avec:

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

1

Ceci est fortement recommandé Obtenir une ligne aléatoire à partir d'une base de données relationnelle

Parce que l'utilisation de django orm pour faire une telle chose rendra votre serveur db en colère, surtout si vous avez une table Big Data: |

Et la solution est de fournir un Model Manager et d'écrire la requête SQL à la main;)

Mettre à jour :

Une autre solution qui fonctionne sur n'importe quel backend de base de données, même non-rel, sans écrire personnalisé ModelManager. Obtenir des objets aléatoires à partir d'un ensemble de requêtes dans Django


1

Vous souhaiterez peut-être utiliser la même approche que celle que vous utiliseriez pour échantillonner n'importe quel itérateur, en particulier si vous prévoyez d'échantillonner plusieurs éléments pour créer un ensemble d'échantillons . @MatijnPieters et @DzinX ont beaucoup réfléchi à ceci:

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 solution de Matijn et DxinX est destinée aux ensembles de données qui ne fournissent pas d'accès aléatoire. Pour les ensembles de données qui fonctionnent (et SQL le fait avec OFFSET), cela est inefficace.
Endre Both

@EndreBoth en effet. J'aime simplement «l'efficacité» de codage d'utiliser la même approche quelle que soit la source de données. Parfois, l'efficacité de l'échantillonnage des données n'affecte pas de manière significative les performances d'un pipeline limité par d'autres processus (quoi que vous fassiez réellement avec les données, comme la formation ML).
plaques de cuisson

1

Une approche beaucoup plus simple consiste simplement à filtrer le jeu d'enregistrements d'intérêt et à random.sampleen sélectionner autant que vous le souhaitez:

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

Notez que vous devez avoir du code en place pour vérifier qu'il my_querysetn'est pas vide; random.samplerenvoie ValueError: sample larger than populationsi le premier argument contient trop peu d'éléments.


2
Cela entraînera-t-il la récupération de l'ensemble de requêtes?
perrohunter

@perrohunter Cela ne fonctionnera même pas avec Queryset(au moins avec Python 3.7 et Django 2.1); vous devez d'abord le convertir en une liste, qui récupère évidemment l'ensemble de la requête.
Endre Both

@EndreBoth - cela a été écrit en 2016, alors qu'aucun de ceux-ci n'existait.
eykanal

C'est pourquoi j'ai ajouté les informations de version. Mais si cela fonctionnait en 2016, il l'a fait en rassemblant l'ensemble de requêtes dans une liste, n'est-ce pas?
Endre Both

@EndreBoth Correct.
eykanal

1

Salut, j'avais besoin de sélectionner un enregistrement aléatoire à partir d'un ensemble de requêtes dont la longueur que je devais également signaler (c'est-à-dire que la page Web a produit l'élément décrit et lesdits enregistrements sont restés)

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

a pris deux fois moins de temps (0,7 s contre 1,7 s) que:

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

J'imagine que cela évite de tirer toute la requête avant de sélectionner l'entrée aléatoire et rend mon système suffisamment réactif pour une page consultée à plusieurs reprises pour une tâche répétitive où les utilisateurs veulent voir le compte à rebours item_count.


0

Méthode d'auto-incrémentation de la clé primaire sans suppression

Si vous avez une table où la clé primaire est un entier séquentiel sans espaces, la méthode suivante devrait fonctionner:

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

Cette méthode est beaucoup plus efficace que les autres méthodes ici qui itèrent sur toutes les lignes de la table. Bien que cela nécessite deux requêtes de base de données, les deux sont triviales. De plus, c'est simple et ne nécessite pas la définition de classes supplémentaires. Cependant, son applicabilité est limitée aux tables avec une clé primaire auto-incrémentée où les lignes n'ont jamais été supprimées, de sorte qu'il n'y a pas de lacunes dans la séquence d'identifiants.

Dans le cas où des lignes ont été supprimées comme des espaces, cette méthode peut toujours fonctionner si elle est réessayée jusqu'à ce qu'une clé primaire existante soit sélectionnée au hasard.

Références


0

J'ai une solution très simple, créer un gestionnaire personnalisé:

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

puis ajoutez le modèle:

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

Maintenant, vous pouvez l'utiliser:

Example.objects.random()

à partir d'un choix d'importation aléatoire
Adam Starrh

3
S'il vous plaît, n'utilisez pas cette méthode, si vous voulez de la vitesse. Cette solution est TRES lente. J'ai vérifié. Il est plus lent que order_by('?').first()plus de 60 fois.
LagRange

@ Alex78191 non, "?" est mauvais aussi, mais ma méthode est EXTRA lente. J'ai utilisé la meilleure solution de réponse.
LagRange
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.