在Django中,从查询集中获取第一个对象的最快方法是什么?

295
通常我发现自己想要从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-测试时会占用很多内存。
我怎样才能只使用一个数据库调用,并且不通过创建异常对象来消耗内存呢?

32
经验法则:如果您担心最小化数据库往返次数,请不要在查询集上使用len(),而应该始终使用.count() - Daniel DiPaolo
8
如果你因为创建一个额外的异常而担心,那么你的做法可能是错误的,因为Python在很多地方都使用异常。你是否实际测试过在你的情况下它是否占用了大量内存?创建异常对象通常会消耗大量内存。 - lqc
1
如果你实际上以任何方式对答案(或至少是评论)进行了基准测试,你就会知道它并不更快。实际上,它可能会更慢,因为你创建了一个额外的列表只是为了将其丢弃。而所有这些都只是相较于首先调用 Python 函数或使用 Django 的 ORM 所需付出的成本而言,微不足道!单个调用 filter() 比引发异常慢得多,很多,非常 多(但依然会被引发,因为这是迭代器协议的工作方式!)。 - lqc
1
你的直觉是正确的,性能差异很小,但你的结论是错误的。我进行了基准测试,事实上接受的答案确实比较快。去想吧。 - Leopd
11
对于使用 Django 1.6 的用户,他们终于添加了 first()last() 工具方法,详情请见:https://docs.djangoproject.com/en/dev/ref/models/querysets/#first - Wei Yen
@DanielDiPaolo - 这只是一个不太准确的经验法则。开发人员需要了解何时评估QuerySets。如果您无论如何都要使用记录,则使用len更好。请参见https://docs.djangoproject.com/en/dev/topics/db/optimization/#don-t-overuse-count-and-exists - spookylukey
9个回答

481

2
它不执行[:1],所以速度不够快(除非你需要评估整个查询集)。 - janek37
26
此外,first()last()在查询中强制执行ORDER BY子句。这将使结果具有确定性,但很可能会减慢查询速度。 - Phil Krylov
@janek37,性能上没有区别。正如cod3monk3y所指出的那样,这是一种方便的方法,它不会读取整个查询集。 - Zompa
1
@Zompa 是错误的。由于 Phil Krylov 指出的强制 ORDER BY,性能存在差异,而 [:1] 可以避免这种情况。 - theannouncer
1
撤销了编辑,因为它除了重新措辞并将原始建议断章取义外没有任何价值。我并不是说first()和last()是最快的方法,仅仅是这些方法存在、有用且方便。并没有声称这会回答OP关于性能的目标。但显然我和其他人发现这些信息略微有用。 - cod3monk3y

173
你可以使用数组切片
Entry.objects.all()[:1].get()

可以与 .filter() 一起使用的是什么:

Entry.objects.filter()[:1].get()
你不需要首先将其转换为列表,因为这将强制执行所有记录的完整数据库调用。只需执行上述操作,它将仅获取第一个。你甚至可以使用.order_by()来确保您获得所需的第一个。

一定要添加.get(),否则你将得到一个QuerySet而不是一个对象。


15
你仍需要在try...except ObjectDoesNotExist中包裹它,这就像原始的第三个选项,但使用了切片。 - Danny W. Adair
1
如果最终还是要调用get(),那么设置LIMIT有什么意义呢?让ORM和SQL编译器决定对其后端最好的方式(例如,在Oracle上,Django模拟LIMIT,因此它会带来伤害而不是帮助)。 - lqc
我使用了这个答案,没有尾随的 .get()。如果返回一个列表,那么我会返回列表的第一个元素。 - Keith John Hutchison
Entry.objects.all()[0]Entry.objects.first() 有什么不同? - James Lin
@JamesLin:没有区别:Entry.objects.all()[:1].get()Entry.objects.all()[0] 都使用 LIMIT 1 并返回一个对象而不是查询集。 - jnns
18
区别在于 [:1].get() 会引发 DoesNotExist 异常,而 [0] 会引发 IndexError 异常。 - Ropez

57

现在,在Django 1.9中,您可以使用first()方法来查询集。

YourModel.objects.all().first()

使用此方法比.get()[0]更好,因为如果查询集为空,它不会引发异常,因此您无需使用exists()进行检查。


1
这会在SQL中引起LIMIT 1,我看到有人声称它可能会使查询变慢 - 尽管我想看到证实:如果查询只返回一个项目,为什么LIMIT 1会影响性能呢?所以我认为上面的答案很好,但很想看到证据证实。 - rrauenza
我不会说“更好”。这真的取决于你的期望。 - trigras
YourModel.objects.first() 不就足够了吗,为什么还要加上 .all() 呢? - Adam Jagosz

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

1
如果您打开跟踪,我相信您甚至会看到将 LIMIT 1 添加到查询中,而且我不知道您是否能做得比这更好。但是,在 QuerySet 中内部实现的 __nonzero__ 是作为 try: iter(self).next() except StopIteration: return false... 实现的,因此它不会逃避异常。 - Ben Jackson
@Ben:由于在检查真实性之前,QuerySet被转换为列表,因此永远不会调用QuerySet.__nonzero__()。但是仍可能发生其他异常。 - Ignacio Vazquez-Abrams
需要先创建一个切片,然后再取第一个元素吗? - Serhii Holinei
@goliney:是的。查询集的切片会向SQL查询添加一个LIMIT子句。 - Ignacio Vazquez-Abrams
14
这个答案现已过时,请参考 @cod3monk3y 关于 Django 1.6+ 的回答。 - ValAyal
显示剩余2条评论

8
这也可以起到作用:
def get_first_element(MyModel):
    my_query = MyModel.objects.all()
    return my_query[:1]

如果为空,则返回一个空列表,否则返回列表中的第一个元素。

1
这绝对是最好的解决方案...只需要一次数据库调用即可得到结果。 - Shh

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,格式为 objects = ManagerWithFirstQuery() - 别忘了加括号 - 总之,你帮了我所以 +1。 - Kamil

4

它可以像这样

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

或者

obj = model.objects.latest('id')

3

你应该使用Django提供的方法,例如exists。这些方法都是为了方便你使用而存在的。

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

2
除非我理解有误,惯用的Python通常使用_Easier to Ask for Forgiveness than Permission_(EAFP)方法,而不是“先看后跳”的方法。 - BigSmoke
EAFP不仅是一种风格建议,它有其原因(例如,在打开文件之前进行检查并不能防止错误)。在这里,我认为相关的考虑是exists + get item会导致两个数据库查询,这可能取决于项目和视图而不受欢迎。 - merwok

0
您可以通过以下方式获取第一个对象:
MyModel.objects.first()
MyModel.objects.all().first()
MyModel.objects.all()[0]
MyModel.objects.filter().first()
MyModel.objects.filter()[0]

此外,您可以通过以下方式获取最后一个对象:
MyModel.objects.last()
MyModel.objects.all().last()
MyModel.objects.filter().last()

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