Django中的select_related - 何时使用?

49
我想优化我的Django ORM查询。我使用connection.queries来查看Django为我生成的查询语句。
假设我有以下这些模型:
class Book(models.Model):
    name   = models.CharField(max_length=50)
    author = models.ForeignKey(Author)

class Author(models.Model):
    name   = models.CharField(max_length=50)

假设我生成一个特定的网页,我想显示所有书籍,并在每本书旁边显示其作者姓名。另外,我单独显示所有作者。

那么我应该使用什么?

Book.objects.all().select_related("author")
会导致一个JOIN查询。即使我在之前加上一行代码:
Author.objects.all()

显然,在模板中我会写类似于{{book.author.name}}的东西。
因此问题是,当我访问外键值(author)时,如果Django已经从另一个查询中拥有了该对象,是否仍会导致额外的查询(对于每本书)? 如果不是这样的话,那么在这种情况下,使用select_related实际上会创建性能开销吗?

4个回答

38

您实际上在问两个不同的问题:

1. 使用select_related是否会导致性能开销?

您应该查看有关Django查询缓存的文档:

了解QuerySet评估

为避免性能问题,重要的是了解:

  • QuerySets是惰性的。

  • 它们何时被评估。

  • 数据如何保存在内存中。

因此,总之,Django在内存中缓存在同一QuerySet对象中评估的结果,也就是说,如果您做这样的事情:

books = Book.objects.all().select_related("author")
for book in books:
    print(book.author.name)  # Evaluates the query set, caches in memory results
first_book = books[1]  # Does not hit db
print(first_book.author.name)  # Does not hit db  

由于您在select_related中预获取了作者,因此只会访问数据库一次,在这些操作的结果中,将会得到带有INNER JOIN的单个数据库查询。

但是,这不会在查询集之间或甚至相同的查询之间进行任何缓存:

books = Book.objects.all().select_related("author")
books2 = Book.objects.all().select_related("author")
first_book = books[1]  # Does hit db
first_book = books2[1]  # Does hit db

这实际上在文档中指出:

我们假设您已经完成了上述明显的事情。本文档的其余部分重点介绍如何以这样的方式使用Django,以便您不必进行不必要的工作。此文档还不涉及适用于所有昂贵操作的其他优化技术,例如通用缓存

2. 如果Django已经从另一个查询获取了该对象,那么是否仍会导致额外的查询(对于每个书籍)?

您实际上是指Django是否具有ORM查询缓存,这是完全不同的问题。ORM查询缓存,即如果您在之前执行查询,然后稍后执行相同的查询,如果数据库未更改,则结果来自缓存而不是 昂贵的数据库查找。

答案是Django不支持官方,但是第三方应用程序可以支持。最相关的第三方应用程序是启用此类缓存的:

  1. Johnny-Cache(较旧,不支持django>1.6)
  2. Django-Cachalot(更新,支持1.6、1.7,仍在开发1.8)
  3. Django-Cacheops(更新,支持Python 2.7或3.3+,Django 1.8+和Redis 2.6+(推荐4.0+))

如果您正在寻找查询缓存,请查看这些内容,并记住,首先对其进行剖析,查找瓶颈,如果它们造成问题,则进行优化。

真正的问题是程序员在错误的地方和错误的时间过于担心效率; 过早的优化是编程中所有罪恶的根源(或至少是大部分)。 Donald Knuth。


我不认为我的意思是整个查询缓存。我是说,因为在之前的查询中我有所有的作者(就像我做Author.objects.all()),所以后来执行book.author.name不应该导致一个查询,因为 Django 可以从作者 queryset 中获取它。但是我意识到这不起作用。 - user3599803
确认您“期望”查询缓存。Django在QuerySet级别进行缓存,如果您有两个对象,每个对象都有不同的缓存。如果您执行Author.objects.all()而没有使用select_related,它将导致一个查询,因为FK是惰性的。如果您添加select_related,则不会再次使用相同的QuerySet对象命中数据库。如果您在代码的另一个点中执行另一个Author.objects.all(),则以前的结果不会被缓存,并且将在新的QuerySet中重新评估。也许当您说之前已经执行了Author.objects.all()时,问题不够清楚。 - danius
我认为select_related使用左外连接而不是内部连接。 - Tolqinbek Isoqov

26
Django 不知道其他查询!Author.objects.all()Book.objects.all() 是完全不同的查询集。如果在视图中同时拥有它们并将它们传递给模板上下文,但在模板中做如下操作:{% for book in books %} {{ book.author.name }} {% endfor %} 并且你有N本书,那么这将导致额外的N个数据库查询(超出获取所有书籍和作者的查询)!
如果你改为使用 Book.objects.all().select_related("author"),则在以上模板片段中不会进行额外的查询。
当然,select_related() 会增加一些查询的开销。发生的情况是,当你执行 Book.objects.all() 时 Django 会返回 SELECT * FROM BOOKS 的结果。如果你改用 Book.objects.all().select_related("author"),Django 将返回以下结果: SELECT * FROM BOOKS B LEFT JOIN AUTHORS A ON B.AUTHOR_ID = A.ID。因此,对于每本书,它将同时返回书籍和其对应的作者两个表格的列。然而,与查询数据库N次(如前所述)相比,这种开销实际上要小得多。
因此,即使select_related 创建了小的性能开销(每次查询都会从数据库返回更多字段),使用它实际上会是有益的,除非你完全确定只需要特定模型的列。
最后,一个真正了解在你的数据库中执行了多少个(以及哪些)查询的好方法是使用 Django Debug Toolbar (https://github.com/django-debug-toolbar/django-debug-toolbar)。

谢谢。如果我的Author对象比较大,也就是说它有更多的字段,而我只需要选择作者的名字与书一起显示。那么这样写是否合法且在性能方面更好呢:Book.objects.all().select_related("author_name"). - user3599803
嗯,我认为这样做是合法且更好的,因为你只会得到所需的字段。但请尝试像这样使用 Book.objects.all().select_related('author__name')。另外,请检查我的更新。 - Serafeim
我不明白你为什么要使用.all() - doniyor
这确实引发了另一个问题,我在哪里使用.all()是否有任何区别?也就是说,在以下两种方式之间是否有区别:Book.objects.all().select_related("author") 或者 Book.objects.select_related("author").all()。或者说,应该尽量避免使用.all()吗?因为我读到过all()是可调用的,因此总是会访问数据库。 - user3599803

7
Book.objects.select_related("author")

足够好。不需要使用Author.objects.all()
{{ book.author.name }}

book.author 已经预先填充,因此不会触及数据库。


我知道这一点。但是我想知道,考虑到我还要查询所有的作者,那么这个JOIN查询是否会浪费资源呢?无论如何,我确实需要Author.objects.all(),就像我说的,在同一页中单独打印了它。 - user3599803
如果你想展示书籍的作者,那么你需要使用select_related(),否则会有更多的数据库查询。 - doniyor
如果您使用Author.objects.all(),它也会访问数据库,从而增加执行时间。 - Roshan Yadav

6

Select_related

select_related是一个可选的性能提升器,用于在Queryset中进一步访问外键属性时,不会触发数据库。

设计哲学

这也是为什么存在select_related() QuerySet方法。它是一个可选的性能提升器,适用于选择“每个相关对象”的常见情况。

Django官方文档

返回一个QuerySet,它将“跟随”外键关系,在执行查询时选择附加的相关对象数据。这是一个性能提升器,它会导致一个更复杂的查询,但意味着以后使用外键关系不需要进行数据库查询。

正如定义中指出的那样,只有在外键关系中才允许使用select_related。忽略此规则将使您面临以下异常:

In [21]: print(Book.objects.select_related('name').all().query)

FieldError: Non-relational field given in select_related: 'name'. Choices are: author

让我们通过一个例子来深入了解:

这是我的models.py。(与之前问的问题一样)

from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=50)

    def __str__(self):
        return self.name

    __repr__ = __str__


class Book(models.Model):
    name = models.CharField(max_length=50)
    author = models.ForeignKey(Author, related_name='books', on_delete=models.DO_NOTHING)

    def __str__(self):
        return self.name

    __repr__ = __str__
  • 使用select_related增强器获取所有书籍及其作者:
In [25]: print(Book.objects.select_related('author').all().explain(verbose=True, analyze=True))
Hash Join  (cost=328.50..548.39 rows=11000 width=54) (actual time=3.124..8.013 rows=11000 loops=1)
  Output: library_book.id, library_book.name, library_book.author_id, library_author.id, library_author.name
  Inner Unique: true
  Hash Cond: (library_book.author_id = library_author.id)
  ->  Seq Scan on public.library_book  (cost=0.00..191.00 rows=11000 width=29) (actual time=0.008..1.190 rows=11000 loops=1)
        Output: library_book.id, library_book.name, library_book.author_id
  ->  Hash  (cost=191.00..191.00 rows=11000 width=25) (actual time=3.086..3.086 rows=11000 loops=1)
        Output: library_author.id, library_author.name
        Buckets: 16384  Batches: 1  Memory Usage: 741kB
        ->  Seq Scan on public.library_author  (cost=0.00..191.00 rows=11000 width=25) (actual time=0.007..1.239 rows=11000 loops=1)
              Output: library_author.id, library_author.name
Planning Time: 0.234 ms
Execution Time: 8.562 ms

In [26]: print(Book.objects.select_related('author').all().query)
SELECT "library_book"."id", "library_book"."name", "library_book"."author_id", "library_author"."id", "library_author"."name" FROM "library_book" INNER JOIN "library_author" ON ("library_book"."author_id" = "library_author"."id")

正如您所见,使用select_related会在提供的外键上(这里是author)执行INNER JOIN

执行时间包括:

  • 使用计划器选择的最快计划运行查询
  • 返回结果

8.562毫秒

另一方面:

  • 获取所有书籍及其作者而不使用select_related优化器:
In [31]: print(Book.objects.all().explain(verbose=True, analyze=True))
Seq Scan on public.library_book  (cost=0.00..191.00 rows=11000 width=29) (actual time=0.017..1.349 rows=11000 loops=1)
  Output: id, name, author_id
Planning Time: 1.135 ms
Execution Time: 2.536 ms

In [32]: print(Book.objects.all().query)
SELECT "library_book"."id", "library_book"."name", "library_book"."author_id" FROM "library_book

正如您所看到的,这只是一个简单的书籍模型上的SELECT查询,其中仅包含author_id。在这种情况下,执行时间为2.536毫秒

正如在Django doc中提到的:

进一步访问外键属性将导致对数据库的另一个访问:(因为我们还没有它们)

In [33]: books = Book.objects.all()

In [34]: for book in books:
    ...:     print(book.author) # Hit the database

请参考数据库访问优化和QuerySet API参考中的explain()

Django数据库缓存:

Django配备了一个强大的缓存系统,可以让您保存动态页面,以便它们不必为每个请求进行计算。为方便起见,Django提供了不同级别的缓存粒度:您可以缓存特定视图的输出,只能缓存难以生成的部分,或者您可以缓存整个站点。

Django还与“下游”缓存(如Squid和基于浏览器的缓存)很好地配合使用。这些是您无法直接控制但可以提供提示(通过HTTP标头)的缓存类型,有关哪些站点部分应该被缓存以及如何缓存。

您应该阅读这些文档,以找出哪个最适合您。


PS1:如需了解计划器及其工作原理的更多信息,请参阅为什么Postgres的计划时间和执行时间差异如此之大?使用EXPLAIN



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