django select_related - 何时使用它

use*_*803 25 django orm select

我正在尝试在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)
Run Code Online (Sandbox Code Playgroud)

比方说,当我生成一个特定的网页时,我希望显示所有书籍,每个书籍旁边都有作者姓名.另外,我单独展示所有作者.

所以我应该使用

Book.objects.all().select_related("author")
Run Code Online (Sandbox Code Playgroud)

这将导致JOIN查询.即使我之前做了一行:

Author.objects.all()
Run Code Online (Sandbox Code Playgroud)

显然在模板中我会写一些类似的东西{{book.author.name}}.
所以问题是,当我访问外键值(作者)时,如果django已经拥有来自另一个查询的那个对象,那还是会导致额外的查询(对于每本书)吗?如果不是,那么在这种情况下,使用select_related实际上是否会产生性能开销?

dan*_*ius 21

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

1.使用select_related确实会产生性能开销吗?

您应该看到有关Django Query Cache的文档:

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

当您在select_related中预取作者时,只会命中db一次,所有这些东西将导致使用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
Run Code Online (Sandbox Code Playgroud)

实际上在文档中指出了这一点:

我们假设您已完成上述明显的事情.本文档的其余部分重点介绍如何以不使用不必要的工作的方式使用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+))

如果您查找查询缓存并记住第一个配置文件,找到瓶颈,如果它们导致问题,那么请查看这些内容然后进行优化.

真正的问题是程序员花费了太多时间来担心在错误的地方和错误的时间提高效率; 过早优化是编程中所有邪恶(或至少大部分)的根源.唐纳德克努特.


Ser*_*eim 18

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-tooblar(https://github.com/django-debug-toolbar/django-debug-toolbar).


Moh*_*umi 7

选择相关

\n

select_related是一个可选的性能增强器,通过它进一步访问Queryset中的foreign_keys 属性,不会影响数据库。

\n

设计理念

\n
\n

这也是 select_lated() QuerySet 方法存在的原因。它\xe2\x80\x99是一个可选的性能增强器,用于选择\xe2\x80\x9ce每个相关对象的常见情况。\xe2\x80\x9d

\n
\n

Django 官方文档

\n
\n

返回一个 QuerySet,它将 \xe2\x80\x9cfollow\xe2\x80\x9d 外键关系,在执行查询时选择其他相关对象数据。这是一个性能增强器,会导致单个更复杂的查询,但意味着稍后使用外键关系将\xe2\x80\x99 不需要数据库查询。

\n
\n

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

\n
In [21]: print(Book.objects.select_related(\'name\').all().query)\n\nFieldError: Non-relational field given in select_related: \'name\'. Choices are: author\n
Run Code Online (Sandbox Code Playgroud)\n

让我们通过一个例子来深入研究它:

\n

这是我的models.py. (这与所问的问题相同)

\n
from django.db import models\n\n\nclass Author(models.Model):\n    name = models.CharField(max_length=50)\n\n    def __str__(self):\n        return self.name\n\n    __repr__ = __str__\n\n\nclass Book(models.Model):\n    name = models.CharField(max_length=50)\n    author = models.ForeignKey(Author, related_name=\'books\', on_delete=models.DO_NOTHING)\n\n    def __str__(self):\n        return self.name\n\n    __repr__ = __str__\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  • 使用助推器获取所有书籍及其作者relect_related
  • \n
\n
In [25]: print(Book.objects.select_related(\'author\').all().explain(verbose=True, analyze=True))\nHash Join  (cost=328.50..548.39 rows=11000 width=54) (actual time=3.124..8.013 rows=11000 loops=1)\n  Output: library_book.id, library_book.name, library_book.author_id, library_author.id, library_author.name\n  Inner Unique: true\n  Hash Cond: (library_book.author_id = library_author.id)\n  ->  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)\n        Output: library_book.id, library_book.name, library_book.author_id\n  ->  Hash  (cost=191.00..191.00 rows=11000 width=25) (actual time=3.086..3.086 rows=11000 loops=1)\n        Output: library_author.id, library_author.name\n        Buckets: 16384  Batches: 1  Memory Usage: 741kB\n        ->  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)\n              Output: library_author.id, library_author.name\nPlanning Time: 0.234 ms\nExecution Time: 8.562 ms\n\nIn [26]: print(Book.objects.select_related(\'author\').all().query)\nSELECT "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")\n\n
Run Code Online (Sandbox Code Playgroud)\n

如您所见,使用 select_lated 会导致在提供的外键上进行INNER JOIN(此处为author)。

\n

执行时间:

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

8.562 毫秒

\n

另一方面:

\n
    \n
  • 不使用 relect_lated 助推器获取所有书籍及其作者:
  • \n
\n
In [31]: print(Book.objects.all().explain(verbose=True, analyze=True))\nSeq Scan on public.library_book  (cost=0.00..191.00 rows=11000 width=29) (actual time=0.017..1.349 rows=11000 loops=1)\n  Output: id, name, author_id\nPlanning Time: 1.135 ms\nExecution Time: 2.536 ms\n\nIn [32]: print(Book.objects.all().query)\nSELECT "library_book"."id", "library_book"."name", "library_book"."author_id" FROM "library_book\n
Run Code Online (Sandbox Code Playgroud)\n

如您所见,这只是对仅包含author_id 的图书模型的简单SELECT查询。在本例中,执行时间为2.536 ms

\n

正如 Django文档中提到的:

\n

进一步访问外键属性将导致数据库再次受到攻击:(因为我们还没有它们)

\n
In [33]: books = Book.objects.all()\n\nIn [34]: for book in books:\n    ...:     print(book.author) # Hit the database\n
Run Code Online (Sandbox Code Playgroud)\n

另请参阅数据库访问优化explain()QuerySet API 参考

\n

Django 数据库缓存:

\n
\n

Django 附带了一个强大的缓存系统,可以让您保存动态页面,这样就不必为每个请求计算它们\xe2\x80\x99t。为了方便起见,Django 提供了不同级别的缓存粒度:您可以缓存特定视图的输出,可以仅缓存难以生成的部分,也可以缓存整个站点。

\n
\n
\n

Django 还可以与 \xe2\x80\x9cdownstream\xe2\x80\x9d 缓存配合良好,例如 Squid 和基于浏览器的缓存。这些是您不直接控制的缓存类型,但您可以(通过 HTTP 标头)提供有关应缓存站点的哪些部分以及如何缓存的提示。

\n
\n

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

\n
\n

PS1:要获得有关规划器及其工作方式的更多信息,请参阅为什么规划时间和执行时间如此不同 Postgres?使用 EXPLAIN

\n
\n


don*_*yor 5

Book.objects.select_related("author")
Run Code Online (Sandbox Code Playgroud)

足够好。不需要Author.objects.all()

{{ book.author.name }}
Run Code Online (Sandbox Code Playgroud)

不会命中数据库,因为book.author已经预填充了。