Le moyen le plus rapide d'obtenir le premier objet d'un ensemble de requêtes dans django?

208

Souvent, je souhaite récupérer le premier objet d'un jeu de requêtes dans Django, ou revenir Nones'il n'y en a pas. Il existe de nombreuses façons de faire cela qui fonctionnent toutes. Mais je me demande lequel est le plus performant.

qs = MyModel.objects.filter(blah = blah)
if qs.count() > 0:
    return qs[0]
else:
    return None

Cela entraîne-t-il deux appels à la base de données? Cela semble inutile. Est-ce plus rapide?

qs = MyModel.objects.filter(blah = blah)
if len(qs) > 0:
    return qs[0]
else:
    return None

Une autre option serait:

qs = MyModel.objects.filter(blah = blah)
try:
    return qs[0]
except IndexError:
    return None

Cela génère un seul appel à la base de données, ce qui est bien. Mais nécessite souvent la création d'un objet d'exception, ce qui est très gourmand en mémoire lorsque tout ce dont vous avez vraiment besoin est un test if trivial.

Comment puis-je faire cela avec un seul appel à la base de données et sans faire de mémoire avec des objets d'exception?

Léopd
la source
22
Règle générale: si vous êtes préoccupé par la réduction des allers-retours de base de données, ne l'utilisez pas len()sur les ensembles de requêtes, utilisez toujours .count().
Daniel DiPaolo
7
«créer souvent un objet d'exception, ce qui est très gourmand en mémoire» - si vous êtes préoccupé par la création d'une exception supplémentaire, alors vous le faites mal car Python utilise des exceptions partout. Avez-vous réellement évalué que cela demande beaucoup de mémoire dans votre cas?
lqc
1
@Leopd Et si vous aviez réellement évalué la réponse de quelque manière que ce soit (ou du moins les commentaires), vous sauriez que ce n'est pas plus rapide. Cela peut en fait être plus lent, car vous créez une liste supplémentaire juste pour la jeter. Et tout cela n'est que des cacahuètes par rapport au coût d'appeler une fonction python ou d'utiliser l'ORM de Django en premier lieu! Un seul appel à filter () est beaucoup, beaucoup, plusieurs fois plus lent que de lever une exception (qui va toujours être levée, car c'est comme ça que fonctionne le protocole itérateur!)
lqc
1
Votre intuition est correcte que la différence de performance est faible, mais votre conclusion est erronée. J'ai exécuté un benchmark et la réponse acceptée est en fait plus rapide et de loin. Allez comprendre.
Leopd
11
Pour les utilisateurs de Django 1.6, ils ont finalement ajouté les méthodes pratiques first()et last(): docs.djangoproject.com/en/dev/ref/models/querysets/#first
Wei Yen

Réponses:

353

Utilisez les méthodes pratiques .first()et .last():

MyModel.objects.filter(blah=blah).first()

Ils avalent tous les deux l'exception résultante et retournent Nonesi le jeu de requêtes ne renvoie aucun objet.

Ceux-ci ont été ajoutés dans Django 1.6, qui a été publié en novembre 2013 .

cod3monk3y
la source
2
il ne fait pas le [: 1], donc ce n'est pas aussi rapide (à moins que vous n'ayez besoin d'évaluer tout le jeu de requêtes de toute façon).
janek37 du
14
également, first()et last()appliquer une ORDER BYclause sur une requête. Cela rendra les résultats déterministes mais ralentira très probablement la requête.
Phil Krylov
@ janek37 il n'y a pas de différences de performances. Comme indiqué par cod3monk3y, c'est une méthode pratique et elle ne lit pas l'ensemble de requêtes.
Zompa du
144

Vous pouvez utiliser le découpage de tableau :

Entry.objects.all()[:1].get()

Qui peut être utilisé avec .filter():

Entry.objects.filter()[:1].get()

Vous ne voudriez pas d'abord le transformer en liste car cela forcerait un appel de base de données complet de tous les enregistrements. Faites simplement ce qui précède et cela ne tirera que le premier. Vous pouvez même l'utiliser .order_by()pour vous assurer d'obtenir le premier que vous voulez.

Assurez-vous d'ajouter le, .get()sinon vous obtiendrez un QuerySet et non un objet.

stormlifter
la source
9
Vous auriez toujours besoin de l'envelopper dans un essai ... sauf ObjectDoesNotExist, qui est comme la troisième option d'origine mais avec découpage.
Danny W.Adair
1
Quel est l'intérêt de définir un LIMIT si vous appelez get () à la fin? Laissez l'ORM et le compilateur SQL décider de ce qui est le mieux pour son backend (par exemple, sur Oracle, Django émule LIMIT, donc ça va faire mal au lieu d'aider).
lqc
J'ai utilisé cette réponse sans le .get () de fin. Si une liste est renvoyée, je renvoie le premier élément de la liste.
Keith John Hutchison
quelle est la différence d'avoir Entry.objects.all()[0]??
James Lin
15
@JamesLin La différence est que [: 1] .get () lève DoesNotExist, tandis que [0] lève IndexError.
Ropez
48
r = list(qs[:1])
if r:
  return r[0]
return None
Ignacio Vazquez-Abrams
la source
1
Si vous activez le traçage, je suis presque sûr que vous verrez même cet ajout LIMIT 1à la requête, et je ne sais pas si vous pouvez faire mieux que cela. Cependant, en interne, __nonzero__in QuerySetest implémenté try: iter(self).next() except StopIteration: return false...pour ne pas échapper à l'exception.
Ben Jackson
@Ben: QuerySet.__nonzero__()n'est jamais appelé car le QuerySetest converti en a listavant de vérifier la justesse. D'autres exceptions peuvent cependant se produire.
Ignacio Vazquez-Abrams
@Aron: Cela peut générer une StopIterationexception.
Ignacio Vazquez-Abrams
conversion en liste === appeler __iter__pour obtenir un nouvel objet itérateur et appeler sa nextméthode jusqu'à ce qu'elle StopIterationsoit lancée. Donc définitivement il y aura une exception quelque part;)
lqc
14
Cette réponse est maintenant obsolète, jetez un œil à la réponse @ cod3monk3y pour Django 1.6+
ValAyal
39

Maintenant, dans Django 1.9, vous avez une first() méthode pour les ensembles de requêtes.

YourModel.objects.all().first()

C'est un meilleur moyen que .get()ou [0]parce qu'il ne lève pas d'exception si le jeu de requêtes est vide, vous n'avez donc pas besoin de vérifier en utilisantexists()

levi
la source
1
Cela provoque une LIMIT 1 dans le SQL et j'ai vu des affirmations selon lesquelles cela peut ralentir la requête - bien que j'aimerais voir cela justifié: si la requête ne renvoie qu'un seul élément, pourquoi la LIMIT 1 devrait-elle vraiment affecter les performances? Je pense donc que la réponse ci-dessus est bonne, mais j'aimerais voir des preuves le confirmer.
rrauenza le
Je ne dirais pas «mieux». Cela dépend vraiment de vos attentes.
trigras le
6

Si vous prévoyez d'obtenir souvent le premier élément, vous pouvez étendre QuerySet dans cette direction:

class FirstQuerySet(models.query.QuerySet):
    def first(self):
        return self[0]


class ManagerWithFirstQuery(models.Manager):
    def get_query_set(self):
        return FirstQuerySet(self.model)

Définissez le modèle comme ceci:

class MyModel(models.Model):
    objects = ManagerWithFirstQuery()

Et utilisez-le comme ceci:

 first_object = MyModel.objects.filter(x=100).first()
Nikolay Fominyh
la source
Appelez les objets = ManagerWithFirstQuery en tant qu'objets = ManagerWithFirstQuery () - N'OUBLIEZ PAS LES PARENTHESES - de toute façon, vous m'avez aidé ainsi +1
Kamil
6

Cela pourrait également fonctionner:

def get_first_element(MyModel):
    my_query = MyModel.objects.all()
    return my_query[:1]

s'il est vide, renvoie une liste vide, sinon il renvoie le premier élément d'une liste.

Nick Cuevas
la source
1
C'est de loin la meilleure solution ... résulte en un seul appel à la base de données
Shh
4

Ça peut être comme ça

obj = model.objects.filter(id=emp_id)[0]

ou

obj = model.objects.latest('id')
Nauman Tariq
la source
3

Vous devriez utiliser les méthodes django, comme existe. Il est là pour que vous puissiez l'utiliser.

if qs.exists():
    return qs[0]
return None
Ari
la source
1
Sauf que, si je comprends bien, Python idiomatique utilise généralement une approche plus facile à demander pardon que l'autorisation ( EAFP ) plutôt qu'une approche Look Before You Leap .
BigSmoke
EAFP n'est pas seulement une recommandation de style, il a des raisons (par exemple, vérifier avant d'ouvrir un fichier n'empêche pas les erreurs). Ici, je pense que la considération pertinente est qu'il existe + get item cause deux requêtes de base de données, ce qui peut être indésirable en fonction du projet et de la vue.
Éric Araujo