在Django中,检查空查询集最有效的方法是什么?

27

我听说可以使用以下方法:

if qs.exists():
    ...

if qs.count():
    ...

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

以下为评论复制内容:“我正在寻找类似于“在MySQL和PostgreSQL中,对于短查询,count()更快,对于长查询,exists()更快,并且当您可能需要第一个元素并想要检查它是否存在时,请使用QuerySet [0]。但是,当count()更快时,速度只有稍微快一点,因此在两者之间进行选择时建议始终使用exists()。”

6个回答

33

query.exists() 是最有效的方法。

特别是在 PostgreSQL 上,count() 可能非常昂贵,有时比普通的 select 查询要更昂贵。

exists() 运行一个没有 select_related、字段选择或排序的查询,并且只获取一条记录。这比计算整个带有表连接和排序的查询要快得多。

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()

另一个让我犯错的陷阱是在if语句中调用QuerySet。这会执行并返回整个查询结果!

如果变量query_set可能为None(函数的未设置参数),则使用以下代码:

if query_set is None:
    # 
not:
if query_set:
   # you just hit the database

12

exists()通常比count()更快,但并非总是如此(请参见下面的测试)。 count()可用于检查存在性和长度。

只有在确实需要对象时才使用qs[0]。如果您仅测试存在性,则速度会慢得多。

在Amazon SimpleDB上,有400,000行:

  • qs:325.00微秒/传递
  • qs.exists():144.46微秒/传递
  • qs.count():144.33微秒/传递
  • qs[0]:324.98微秒/传递

在MySQL上,有57行:

  • qs:1.07微秒/传递
  • qs.exists():1.21微秒/传递
  • qs.count():1.16微秒/传递
  • qs[0]:1.27微秒/传递

我对每个传递使用了随机查询,以降低数据库级缓存的风险。测试代码:

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)

13
在SQL中,exists会在找到匹配记录后立即停止扫描表,而count则被强制要求扫描整个表。基于这一点,我原本认为exists会更快。 - super9
13
这完全不是真的。在Postgres上,count()函数特别耗费资源,有时比普通的select查询还要更昂贵。exists()函数运行一个没有select_related或字段选择的查询,并仅获取单个记录。它要快得多。源代码在这里(django / db / models / sql / query.py RawQuery.has_results):https://github.com/django/django/blob/master/django/db/models/sql/query.py#L438 - Chris Sattinger
6
这绝对不是真的。EXISTS更有效率。 - Tom
@Sam Odio,我认为你的方法有一些缺陷,我已经在https://dev59.com/nGw15IYBdhLWcg3wCnQn#43388116中试图解决。 - DanH
存在比计数检查存在要快几个数量级,经过实证测试,这个答案是误导性的。 - Makogan

11

这取决于使用环境。

根据文档

使用 QuerySet.count()

...如果你只需要计数,而不是用 len(queryset)。

使用 QuerySet.exists()

...如果你只想知道是否至少存在一个结果,而不是查询集的信息。

但:

不要过度使用 count() 和 exists()

如果你以后需要查询集中的其他数据,就直接评估它。

因此,我认为如果你只想检查空的查询集,QuerySet.exists()是最推荐的方法。另一方面,如果你以后需要使用结果,最好将其评估。

我也认为第三种方法是最昂贵的,因为你需要检索所有记录只为了检查是否存在任何记录。


我非常确定最后一个选项只会获取一条记录,而不是所有记录。此外,从文档中并不清楚exists比count更快。如果是这样的话,我很想知道速度差异以及是否基于条件(如查询集的长度)有显著变化。 - Sam Odio
你是对的 - 最后一个选项将被翻译成类似于 SELECT foo FROM bar LIMIT 1 的内容。这就是问题所在 - 所有这些方法都是ORM API调用,并转换为SQL查询。这就是为什么文档无法说“exists”比“count”更快,或者它快多少倍。这取决于数据库实现、其配置和相关数据集。我认为Clueless是正确的 - 你需要自己测试才能得到确切的结果。 - Marcin Świerczyński
1
我不想每次使用这些东西时都进行测试。只要有一个大致的性能差异范围就好了。例如,我正在寻找像“在MySQL和PostgreSQL中,对于短查询,count()更快,对于长查询,exists()更快,并且在可能需要第一个元素并且想要检查其是否存在时,请使用QuerySet[0]。但是,当count()更快时,它只是稍微快一点,因此在两者之间进行选择时建议始终使用exists()。”这样的语句。 - Sam Odio

6

@Sam Odio的解决方案是一个不错的起点,但方法有一些缺陷,具体来说:

  1. 随机IP地址可能会匹配0个或非常少的结果
  2. 异常会使结果偏差,因此我们应该避免处理异常

因此,我决定排除肯定不匹配的内容,而不是过滤可能匹配的内容,希望仍然避免DB缓存,但也确保相同数量的行。

我只在本地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() 大约比 exists() 慢了9倍。 [0] 也很快,但需要异常处理。

1

我想第一种方法是最有效的方式(你可以很容易地用第二种方法来实现它,所以它们可能几乎相同)。最后一种方法需要从数据库中获取整个对象,因此几乎肯定是最昂贵的。

但是,像所有这些问题一样,唯一知道适合您特定数据库、模式和数据集的方法就是自己测试。


1
我也遇到过这个问题。是的,对于大多数情况,exists()更快,但它很大程度上取决于你要查询的数据集类型。例如,对于一个简单的查询:my_objects = MyObject.objets.all(),你应该使用my_objects.exists()。但是,如果你要进行这样的查询:MyObject.objects.filter(some_attr='anything').exclude(something='what').distinct('key').values(),你可能需要测试哪个更适合(exists()count()len(my_objects))。请记住,数据库引擎将执行查询,并且为了获得良好的性能结果,它非常依赖于数据结构和查询的形式。你可以做的一件事是,审计查询并针对数据库引擎进行测试,并比较你的结果,你会惊讶地发现有时django是多么天真,尝试使用QueryCountMiddleware查看所有执行的查询,你就会明白我在说什么。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接