В Django, какой самый эффективный способ проверить пустой набор запросов?

Я слышал предложения использовать следующее:

if qs.exists():
    ...

if qs.count():
    ...

try:
    qs[0]
except IndexError:
    ...

Скопировано из комментария ниже: "Я ищу инструкцию типа" В MySQL и PostgreSQL count() быстрее для коротких запросов, существует() быстрее для длинных запросов и использует QuerySet [0], когда это вероятно, что вам понадобится первый элемент, и вы хотите проверить, что он существует. Однако, когда count() работает быстрее, он только немного быстрее, поэтому всегда рекомендуется использовать exists() при выборе между ними.

Ответы

Ответ 1

Похоже, что qs.count() и qs.exists() эффективно эквивалентны. Поэтому я не нашел причины использовать exists() над count(). Последний не медленнее, и его можно использовать для проверки как существования, так и длины. Возможно, что оба метода exists() и count() оцениваются в одном и том же запросе в MySQL.

Используйте qs[0] только в том случае, если вам действительно нужен объект. Это значительно медленнее, если вы просто проверяете существование.

На Amazon SimpleDB, 400 000 строк:

  • голый qs: 325,00 usec/pass
  • qs.exists(): 144.46 usec/pass
  • qs.count() 144.33 usec/pass
  • qs[0]: 324.98 usec/pass

В MySQL, 57 строк:

  • голый qs: 1.07 usec/pass
  • qs.exists(): 1.21 usec/pass
  • qs.count(): 1.16 usec/pass
  • qs[0]: 1.27 usec/pass

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

import timeit

base = """
import random
from plum.bacon.models import Session
ip_addr = str(random.randint(0,256))+'.'+str(random.randint(0,256))+'.'+str(random.randint(0,256))+'.'+str(random.randint(0,256))
try:
    session = Session.objects.filter(ip=ip_addr)%s
    if session:
        pass
except:
    pass
"""

query_variatons = [
    base % "",
    base  % ".exists()",
    base  % ".count()",
    base  % "[0]"
    ]

for s in query_variatons:
    t = timeit.Timer(stmt=s)
    print "%.2f usec/pass" % (1000000 * t.timeit(number=100)/100000)

Ответ 2

query.exists() является наиболее эффективным способом.

Особенно в postgres count() может быть очень дорого, иногда дороже, чем обычный запрос выбора.

exists() запускает запрос без выбора по выбору, выбора поля или сортировки и извлекает только одну запись. Это намного быстрее, чем подсчет всего запроса с объединением таблиц и сортировкой.

qs[0] будет по-прежнему включать select_related, выбор полей и сортировку; так что это будет дороже.

Исходный код Django находится здесь (django/db/models/sql/query.py RawQuery.has_results):

https://github.com/django/django/blob/60e52a047e55bc4cd5a93a8bd4d07baed27e9a22/django/db/models/sql/query.py#L499

def has_results(self, using):
    q = self.clone()
    if not q.distinct:
        q.clear_select_clause()
    q.clear_ordering(True)
    q.set_limits(high=1)
    compiler = q.get_compiler(using=using)
    return compiler.has_results()

Еще один вопрос, который получил меня на днях, вызывает QuerySet в выражении if. Это выполняет и возвращает весь запрос!

Если переменная query_set может быть None (аргумент unset для вашей функции), используйте:

if query_set is None:
    # 

not:

if query_set:
   # you just hit the database

Ответ 3

Это зависит от контекста использования.

Согласно документации:

Использовать QuerySet.count()

... если вам нужен только счет, а не делать len (queryset).

Использовать QuerySet.exists()

... если вы хотите узнать, существует ли хотя бы один результат, а не запрос.

Но:

Не злоупотребляйте count() и существует()

Если вам понадобятся другие данные из QuerySet, просто оцените его.

Итак, я думаю, что QuerySet.exists() является наиболее рекомендуемым способом, если вы просто хотите проверить пустой QuerySet. С другой стороны, если вы хотите использовать результаты позже, лучше оценить его.

Я также считаю, что ваш третий вариант является самым дорогим, потому что вам нужно получить все записи, чтобы проверить, существует ли какой-либо файл.

Ответ 4

@Решение Sam Odio было достойной отправной точкой, но есть несколько недостатков в методологии, а именно:

  • Случайный IP-адрес может привести к совпадению 0 или очень мало результатов.
  • Исключение будет искажать результаты, поэтому мы должны стремиться избегать обработки исключений.

Поэтому вместо того, чтобы фильтровать что-то, что могло бы совпадать, я решил исключить то, что определенно не будет соответствовать, но, надеюсь, все равно избегать кеша БД, но также обеспечивает одинаковое количество строк.

Я тестировал только локальную базу данных MySQL с набором данных:

>>> Session.objects.all().count()
40219

Временной код:

import timeit
base = """
import random
import string
from django.contrib.sessions.models import Session
never_match = ''.join(random.choice(string.ascii_uppercase) for _ in range(10))
sessions = Session.objects.exclude(session_key=never_match){}
if sessions:
    pass
"""
s = base.format('count')

query_variations = [
    "",
    ".exists()",
    ".count()",
    "[0]",
]

for variation in query_variations:
    t = timeit.Timer(stmt=base.format(variation))
    print "{} => {:02f} usec/pass".format(variation.ljust(10), 1000000 * t.timeit(number=100)/100000)

выходы:

           => 1390.177710 usec/pass
.exists()  => 2.479579 usec/pass
.count()   => 22.426991 usec/pass
[0]        => 2.437079 usec/pass

Итак, вы можете видеть, что count() примерно в 9 раз медленнее, чем exists() для этого набора данных.

[0] также работает быстро, но ему нужна обработка исключений.

Ответ 5

Я бы предположил, что первый метод является наиболее эффективным способом (его можно легко реализовать в терминах второго метода, поэтому, возможно, они почти идентичны). Последнее требует фактически получить весь объект из базы данных, поэтому он почти наверняка самый дорогой.

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