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

Sam*_*dio 16 sql django performance django-queryset

我听说过使用以下内容的建议:

if qs.exists():
    ...

if qs.count():
    ...

try:
    qs[0]
except IndexError:
    ...
Run Code Online (Sandbox Code Playgroud)

复制下面的评论:"我正在寻找一个声明,如"在MySQL中,PostgreSQL count()对于短查询更快,exists()对于长查询更快,并且当你很可能使用QuerySet [0]时需要第一个元素,你想检查它是否存在.但是,当count()更快时,它只会稍微快一些,所以建议在两者之间选择时始终使用exists()."

fel*_*lix 16

query.exists() 是最有效的方式.

特别是在postgres count()上可能非常昂贵,有时比普通的选择查询更昂贵.

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()
Run Code Online (Sandbox Code Playgroud)

前几天得到我的另一个问题是在if语句中调用QuerySet.执行并返回整个查询!

如果变量query_set可能是None(函数的未设置参数),那么使用:

if query_set is None:
    # 
Run Code Online (Sandbox Code Playgroud)

不:

if query_set:
   # you just hit the database
Run Code Online (Sandbox Code Playgroud)


Sam*_*dio 14

看起来qs.count()和qs.exists()实际上是等价的.因此我没有发现使用exists()而不是count()的原因.后者并不慢,它可以用来检查存在和长度.mem()和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

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

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)
Run Code Online (Sandbox Code Playgroud)

  • 在SQL中,只要找到匹配的记录,exists就会停止扫描表,而强制计数将扫描整个表.在那个数字上,我原以为存在会更快. (12认同)
  • 这根本不是真的.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 (10认同)
  • 这绝对不是真的.[`EXISTS`](http://sqlblog.com/blogs/andrew_kelly/archive/2007/12/15/exists-vs-count-the-battle-never-ends.aspx)效率更高. (2认同)

Mar*_*ski 7

这取决于使用环境.

根据文件:

使用QuerySet.count()

...如果你只想要计数,而不是做len(queryset).

使用QuerySet.exists()

...如果您只想知道是否存在至少一个结果,而不是查询集.

但:

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

如果您需要QuerySet中的其他数据,请对其进行评估.

因此,QuerySet.exists()如果您只想检查空QuerySet ,我认为这是最推荐的方法.另一方面,如果您想稍后使用结果,最好对其进行评估.

我还认为您的第三个选项是最昂贵的,因为您需要检索所有记录以检查是否存在.


Dan*_*anH 5

@Sam Odio 的解决方案是一个不错的起点,但该方法存在一些缺陷,即:

  1. 随机 IP 地址可能最终匹配 0 个或很少的结果
  2. 异常会扭曲结果,所以我们应该尽量避免处理异常

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

我只针对本地 MySQL 数据库进行了测试,数据集为:

>>> Session.objects.all().count()
40219
Run Code Online (Sandbox Code Playgroud)

计时码:

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)
Run Code Online (Sandbox Code Playgroud)

输出:

           => 1390.177710 usec/pass
.exists()  => 2.479579 usec/pass
.count()   => 22.426991 usec/pass
[0]        => 2.437079 usec/pass
Run Code Online (Sandbox Code Playgroud)

所以你可以看到它count()exists()这个数据集慢了大约 9 倍。

[0] 也很快,但需要异常处理。