Самый быстрый способ получить первый объект из набора запросов в Django?


193

Часто мне хочется получить первый объект из набора запросов в Django или вернуть его, Noneесли его нет. Есть много способов сделать это, которые все работают. Но мне интересно, какой из них наиболее производительный.

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

Это приводит к двум вызовам базы данных? Это кажется расточительным. Это быстрее?

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

Другой вариант будет:

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

Это генерирует один вызов базы данных, что хорошо. Но требует создания объекта исключения в большинстве случаев, что требует очень много памяти, когда все, что вам действительно нужно, - это тривиальный if-тест.

Как я могу сделать это с помощью всего одного вызова базы данных и без использования памяти с объектами исключений?


21
Практическое правило. Если вы беспокоитесь о минимизации обходов БД, не используйте len()наборы запросов, всегда используйте .count().
Даниэль ДиПаоло

7
«создание объекта исключения в большинстве случаев, что требует очень много памяти» - если вас беспокоит создание одного дополнительного исключения, то вы делаете это неправильно, поскольку Python использует исключения повсеместно. Вы на самом деле отметили, что это интенсивно использует память в вашем случае?
LQC

1
@Leopd И если бы вы на самом деле тестировали ответ (или, по крайней мере, комментарии), вы бы знали, что это не так быстро. Это на самом деле может быть медленнее, потому что вы создаете дополнительный список просто чтобы выбросить его. И все это просто арахис по сравнению со стоимостью вызова функции Python или использования ORM в Django! Один вызов filter () намного, во много, много раз медленнее, чем вызывает исключение (которое все еще будет вызываться, потому что именно так работает протокол итератора!).
LQC

1
Ваша интуиция верна, что разница в производительности невелика, но ваш вывод неверен. Я выполнил тест, и принятый ответ на самом деле быстрее с большим отрывом. Пойди разберись.
Леопд

11
Для людей, использующих Django 1.6, они наконец-то добавили first()и last()удобные методы: docs.djangoproject.com/en/dev/ref/models/querysets/#first
Вей Йен,

Ответы:


328

Django 1.6 (выпущен ноябрь 2013) представил удобные методы first() и last()которые глотают получившееся исключение и возврат , Noneесли QuerySet не возвращает объектов.


2
он не выполняет [: 1], поэтому он не такой быстрый (если вам все равно не нужно оценивать весь набор запросов).
января

13
Кроме того, first()и last()обеспечить соблюдение ORDER BYусловия в запросе. Это сделает результаты детерминированными, но, скорее всего, замедлит запрос.
Фил Крылов

@ janek37 нет различий в производительности. Как указывает cod3monk3y, это удобный метод, который не читает весь набор запросов.
Zompa

143

Правильный ответ

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

Который может быть использован в:

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

Вы не хотели бы сначала превратить его в список, потому что это вызвало бы полный вызов базы данных всех записей. Просто сделайте выше, и он будет тянуть только первым. Вы могли бы даже использовать, .order_byчтобы получить первое, что вы хотите.

Обязательно добавьте, .get()иначе вы получите QuerySet, а не объект.


9
Вам все равно нужно будет обернуть его в попытке ... за исключением ObjectDoesNotExist, который похож на оригинальный третий вариант, но с нарезкой.
Дэнни В. Адайр

1
Какой смысл устанавливать LIMIT, если в конце концов ты собираешься вызвать get ()? Пусть ORM и компилятор SQL решат, что лучше для его бэкэнда (например, в Oracle Django эмулирует LIMIT, так что это будет больно, а не помогать).
Жк

Я использовал этот ответ без завершающего .get (). Если список возвращается, я возвращаю первый элемент списка.
Кит Джон Хатчисон

в чем разница Entry.objects.all()[0]?
Джеймс Лин

15
@JamesLin Разница заключается в том, что [: 1] .get () вызывает DoesNotExist, а [0] - IndexError.
Ропез

49
r = list(qs[:1])
if r:
  return r[0]
return None

1
Если вы включите трассировку, я почти уверен, что вы даже увидите это дополнение LIMIT 1к запросу, и я не знаю, что вы можете сделать что-то лучше этого. Тем не менее, внутренне __nonzero__в QuerySetреализуются как try: iter(self).next() except StopIteration: return false...так не избежать исключений.
Бен Джексон

@Ben: QuerySet.__nonzero__()никогда не вызывается, поскольку QuerySetпреобразуется в a listперед проверкой на правильность. Однако могут возникнуть и другие исключения.
Игнасио Васкес-Абрамс

@ Арон: Это может вызвать StopIterationисключение.
Игнасио Васкес-Абрамс

преобразование в список === вызов, __iter__чтобы получить новый объект итератора и вызывать его nextметод, пока не StopIterationбудет брошен. Так что определенно будет где-то исключение;)
lqc

14
Этот ответ устарел, посмотрите на @ cod3monk3y ответ для Django 1.6+
ValAyal

37

Теперь в Django 1.9 у вас есть first() метод для наборов запросов.

YourModel.objects.all().first()

Это лучший способ, чем .get()или [0]потому что он не выдает исключение, если набор запросов пуст, поэтому вам не нужно проверять, используяexists()


1
Это вызывает LIMIT 1 в SQL, и я видел утверждения, что это может сделать запрос медленнее - хотя я хотел бы видеть это обоснованным: если запрос возвращает только один элемент, почему LIMIT 1 действительно влияет на производительность? Поэтому я думаю, что приведенный выше ответ - это хорошо, но хотелось бы, чтобы доказательства подтвердились.
rrauenza

Я бы не сказал «лучше». Это действительно зависит от ваших ожиданий.
Тригра

7

Если вы планируете часто получать первый элемент - вы можете расширить QuerySet в этом направлении:

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


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

Определите модель следующим образом:

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

И используйте это так:

 first_object = MyModel.objects.filter(x=100).first()

Вызовите объекты = ManagerWithFirstQuery как объекты = ManagerWithFirstQuery () - НЕ ЗАБЫВАЙТЕ РОДИТЕЛЕЙ - в любом случае, вы мне так помогли +1
Камил

7

Это также может сработать:

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

если он пустой, то возвращает пустой список, в противном случае он возвращает первый элемент в списке.


1
На сегодняшний день это лучшее решение ... в результате всего один вызов базы данных
Shh


3

Вы должны использовать методы Django, как существует. Его там для вас, чтобы использовать его.

if qs.exists():
    return qs[0]
return None

1
За исключением того, что, если я правильно понимаю, идиоматический Python обычно использует подход « Проще просить прощения, чем разрешения» ( EAFP ), а не подход « Взгляд перед вами» .
BigSmoke

EAFP - это не просто рекомендация по стилю, у него есть причины (например, проверка перед открытием файла не предотвращает ошибок). Здесь я думаю, что уместно учитывать, что существует + получить элемент вызывают два запроса к базе данных, что может быть нежелательным в зависимости от проекта и представления.
Эрик Араужо

2

Начиная с django 1.6, вы можете использовать filter () с методом first () следующим образом:

Model.objects.filter(field_name=some_param).first()
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.