Django ORM性能优化,数据存取优化

2019-02-12  本文已影响6人  吾星喵

Django ORM性能优化,数据存取优化

https://docs.djangoproject.com/en/2.2/topics/db/optimization/

Web系统可以采用自底向上的顺序,从数据存储一直到数据展现,按照这个顺序一点一点查找性能问题。

  1. 数据库 (缺少索引/数据模型)
  2. 数据存储接口 (ORM/低效的查询)
  3. 展现/数据使用 (Views/报表等)

工具

解决问题的第一步是找到问题

运行Django看到原始SQL查询

确保Django DEBUG设置为True。然后,就这样做:

>>> from django.db import connection
>>> connection.queries
[]
>>> Author.objects.all()
<QuerySet [<Author: Author object>]>
>>> connection.queries
[{u'time': u'0.002', u'sql': u'SELECT "library_author"."id", "library_author"."name" FROM "library_author" LIMIT 21'}]

connection.queries只有在调试为真时。它是一个按查询执行顺序排列的字典列表。每个词典都有以下内容:

``sql`` -- The raw SQL statement
``time`` -- How long the statement took to execute, in seconds.

connection.queries查询包括所有SQL语句——插入、更新、选择等。每次应用程序访问数据库时,查询都会被记录下来。

如果您使用多个数据库,您可以在connections字典的每个成员上使用相同的接口:

>>> from django.db import connections
>>> connections['my_db_alias'].queries

如果需要在函数的任意位置手动清除查询列表,只需调用reset_queries(),如下所示:

from django.db import reset_queries
reset_queries()

但是使用起来好像不是很方便。

django-exensions显示原始SQL

在shell命令行的环境下,可以使用 django-exensionsshell_plus 命令并打开--print-sql 选项。

python manage.py shell_plus --print-sql

运行该shell后可以在命令行输入查询语句,会自动打印SQL内容。

>>> Author.objects.all()
SELECT "library_author"."id", "library_author"."name" FROM "library_author" LIMIT 21
Execution time: 0.001393s [Database: default]
<QuerySet [<Author: Author object>]>

调试配置面板django-debug-toolbar

还有个更方便的方式, 使用 django-debug-toolbar 工具,就可以在web端查看SQL查询的详细统计结果,其实它功能远不止这个。

总结下3个方式

数据存取优化(官译)

Django的数据库层提供了多种方法来帮助开发人员最大限度地利用他们的数据库。本文收集了相关文档的链接,并添加了各种技巧,概述了优化数据库使用时要采取的步骤。

概述

找出你正在做的查询和它们的成本。使用QuerySet.explain()了解数据库如何执行特定的QuerySet。可能还希望使用外部项目,如django-debug-toolbar,或者直接监视数据库的工具。

根据需求,可能正在优化速度或内存,或者两者兼而有之。有时对其中一个进行优化会对另一个有害,但有时它们会互相帮助。此外,数据库进程所做的工作的成本(对您来说)可能与Python进程中所做的工作的数量不同。由自己决定优先级是什么,平衡点在哪里,并根据需要对这些优化进行概要分析,因为这将取决于应用程序和服务器。

对于接下来的所有内容,请记住在每次更改之后进行概要分析,以确保更改是一种好处,并且在代码可读性下降的情况下,这种好处是足够大的。

使用标准的数据库优化技术

我们假设已经完成了上述显而易见的事情。本文档的其余部分将重点介绍如何以一种不做不必要工作的方式使用Django。本文也不讨论适用于所有昂贵操作的其他优化技术,如通用缓存。

理解QuerySets

理解查询集对于使用简单的代码获得良好的性能至关重要。特别是:

理解QuerySet计算

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

QuerySet是惰性的——创建QuerySet的行为不涉及任何数据库活动。您可以整天将筛选器堆叠在一起,Django在计算QuerySet之前不会实际运行查询。看看这个例子:

>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q = q.exclude(body_text__icontains="food")
>>> print(q)

虽然这看起来像是三次数据库连接,但实际上它只连接数据库一次,在最后一行(print(q))。通常,QuerySet的结果在“请求”之前不会从数据库中获取。当这样做时,将通过访问数据库来计算QuerySet。有关准确计算时间的详细信息,请查看何时计算查询集

!理解缓存属性

除了缓存整个QuerySet之外,还缓存ORM对象上的属性结果。通常,将缓存不可调用的属性。例如,假设示例Weblog模型:

>>> entry = Entry.objects.get(id=1)
>>> entry.blog   # 此时将检索Blog对象
>>> entry.blog   # 缓存的版本,没有数据库访问

但一般来说,可调用属性每次都会导致DB查找:

>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all()   # 查询执行
>>> entry.authors.all()   # 查询再次执行

读取模板代码时要小心——模板系统不允许使用括号(),但会自动调用可调用项,隐藏了上述区别。

注意自己的自定义属性——在需要时由您来实现缓存,例如使用cached_property装饰器。

使用模板标签

要使用QuerySet的缓存行为,可能需要使用with模板标签。

!使用iterator()保持连接查询

当查询结果有很多对象时,QuerySet的缓存行为会导致使用大量内存。如果你需要对查询结果进行好几次循环,这种缓存是有意义的,但是对于 queryset 只循环一次的情况,缓存就没什么意义了。在这种情况下,iterator()可能会有所帮助。

for book in Books.objects.all():
    do_xxx(book)

上面的查询,Django会把Books的所有数据载入内存,然后进行一次循环,其实我们更想要保持这个数据库 connection, 每次循环的取出一条book数据,然后调用 do_xxx操作。iterator 就是我们的救星。

for book in Books.objects.all().iterator():
    do_xxx(book)

有了 iterator,你就可以编写线性数据表或者CSV流了。就能增量写入文件或者发送给用户。

特别是跟 valuesvalues_list 结合在一起的时候,能尽可能少的使用内存。在需要对表中的每一行进行修改的迁移期间,使用iterator也非常方便。 不能因为迁移不是面向客户的就可以降低对效率的要求。 长时间运行的迁移可能意味着事务锁定或停机。

使用explain()

QuerySet.explain()提供关于数据库如何执行查询的详细信息,包括使用的索引和连接。这些细节可以帮助您找到可以更有效地重写的查询,或者确定可以添加哪些索引来提高性能。

操作在数据库完成而不是Python中

例如:

如果这些还不足以生成所需的SQL:

使用RawSQL

一个不太容易移植但功能更强大的方法是RawSQL表达式,它允许显式地将一些SQL添加到查询中。如果这还不够强大:

使用原始SQL

编写您自己的自定义SQL来检索数据或填充模型。使用django.db.connection.queries以找出Django为您编写了什么,并从那里开始。

!用唯一的被索引的列来检索独立对象

在使用get()检索单个对象时,使用具有uniquedb_index的列有两个原因。首先,由于底层数据库索引,查询将更快。此外,如果多个对象匹配查找,查询可能会运行得慢得多;在列上有唯一的约束可以保证这永远不会发生。

使用Weblog模型的例子:

>>> entry = Entry.objects.get(id=10)

将比下面的更快

>>> entry = Entry.objects.get(headline="News Item Title")

因为id是由数据库索引的,并且保证是惟一的。

做下面的事情可能会很慢:

>>> entry = Entry.objects.get(headline__startswith="News")

首先,headline没有被索引,这将使底层数据库获取速度变慢。

其次,查找不能保证只返回一个对象。如果查询匹配多个对象,它将从数据库中检索和传输所有对象。如果数百或数千条记录被返回,那么这种惩罚可能是巨大的。如果数据库位于一个单独的服务器上,那么损失将更加严重,网络开销和延迟也是一个因素。

一次检索所有的情况

通常情况下,对于需要全部数据的一组数据的不同部分,多次访问数据库比在一次查询中检索所有数据效率要低。如果有一个在循环中执行的查询,并且最终可能执行许多数据库查询,而只需要一个查询,那么这一点尤其重要。所以:

使用QuerySet.select_related() 和 prefetch_related()

彻底理解select_related()prefetch_related(),并使用它们:

!select_related(*fields)

返回一个QuerySet,当执行它的查询时,它沿着外键关系查询关联的对象的数据。它会生成一个复杂的查询并引起性能的损耗,但是在以后使用外键关系时将不需要数据库查询。

对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用select_related 来对QuerySet进行优化,在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。

示例1:

# 连接数据库
e = Entry.objects.get(id=5)

# 再次连接数据库以获取相关的Blog对象
b = e.blog
# 连接数据库
e = Entry.objects.select_related('blog').get(id=5)

# 没有进入数据库,因为e.blog已经预填充在前面的查询中。
b = e.blog

示例二:

Host.objects.select_related("business").filter(user__username=user_name).count()
  1. select_related主要针一对一和多对一关系进行优化。
  2. select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
  3. 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。没有指定的字段不会缓存,没有指定的深度不会缓存,如果要访问的话Django会再次进行SQL查询。
  4. 也可以通过depth参数指定递归的深度,Django会自动缓存指定深度内所有的字段。如果要访问指定深度外的字段,Django会再次进行SQL查询。
  5. 也接受无参数的调用,Django会尽可能深的递归查询所有的字段。但注意有Django递归的限制和性能的浪费。

prefetch_related(*lookups)

这允许它预取多对多和多对一对象,因为它们不能使用select_related来完成。

select_related主要是执行跨表操作,而prefetch_related不是跨表操作,而是对每个表分别执行SQL查询(Django内部在您使用时自动会帮你像跨表操作一样连接起来),从而减少跨表查询,提升性能。

小结

  1. 因为select_related()总是在单次SQL查询中解决问题,而prefetch_related()会对每个相关表进行SQL查询,因此select_related()的效率通常比后者高。
  2. 鉴于第一条,尽可能的用select_related()解决问题。只有在select_related()不能解决问题的时候再去想prefetch_related()
  3. 你可以在一个QuerySet中同时使用select_related()prefetch_related(),从而减少SQL查询的次数。
  4. 只有prefetch_related()之前的select_related()是有效的,之后的将会被无视掉。

无需获取不使用字段

!使用QuerySet.values() 和 values_list()

当您只想要一个dict或值list,而不需要ORM模型对象时,请适当地使用values()。这些可以用于替换模板代码中的模型对象——只要您提供的dicts与模板中使用的dicts具有相同的属性,就没有问题。values()结果为列表中的字典的查询集, values_list()结果为列表中的元组的查询集

使用QuerySet.defer() 和 only()

当您知道不需要(或在大多数情况下不需要)某些数据库列来避免加载它们时,才使用defer()only()。请注意,如果确实使用它们,ORM将不得不在一个单独的查询中获取它们,如果您不恰当地使用它们,那么这将是一个悲观化。

另外,请注意,在使用延迟字段构造模型时,Django内部会产生一些(小的额外)开销。在不进行概要分析的情况下,不要过于激进地延迟字段,因为数据库必须从磁盘中为结果中的一行读取大多数非文本、非varchar数据,即使最后只使用了几列。defer()only()方法在可以避免加载大量文本数据或对于可能需要大量处理才能转换回Python的字段时最有用。像往常一样,首先配置文件,然后优化。

!使用 QuerySet.count()

如果您只想要计数,而不是len(queryset)

!使用 QuerySet.exists()

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

blogs = Blog.objects.filter(author='xxx')

# if blogs:  # 不要使用这个方法
if blogs.exists():  # 使用exists()判断是否存在至少一个结果
    do_xxx()

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

如果您需要QuerySet中的其他数据,只需计算它。

例如,假设电子邮件模型具有body属性和与用户的多对多关系,下面的模板代码是最优的:

{% if display_inbox %}
  {% with emails=user.emails.all %}
    {% if emails %}
      <p>You have {{ emails|length }} email(s)</p>
      {% for email in emails %}
        <p>{{ email.body }}</p>
      {% endfor %}
    {% else %}
      <p>No messages today.</p>
    {% endif %}
  {% endwith %}
{% endif %}

它是最优的,因为:

  1. 由于QuerySets是惰性的,因此如果display_inbox为假,则不执行数据库查询。
  2. with表示我们存储user.email.all这些都在一个变量中供以后使用,允许其缓存被重用。
  3. 这一行{% if email %}导致调用QuerySet.__bool__(),这将导致在数据库上运行user. mail .all()查询,至少第一行将转换为ORM对象。如果没有任何结果,它将返回False,否则返回True。
  4. 使用{{ email|length }}调用QuerySet.__len__(),在不执行另一个查询的情况下填充缓存的其余部分。
  5. for循环遍历已填充的缓存。

总之,这段代码执行一个或零个数据库查询。唯一经过深思熟虑的优化是使用with标记。在任何时候使用QuerySet.exists()QuerySet.count()都会导致额外的查询。

!使用QuerySet.update() 和 delete()

与其检索一堆对象、设置一些值并单独保存它们,不如通过QuerySet.update()使用批量SQL UPDATE语句。类似地,尽可能进行批量删除。

但是,请注意,这些批量更新方法不能调用单个实例的save()delete()方法,这意味着不会执行为这些方法添加的任何自定义行为,包括从普通数据库对象信号驱动的任何行为。

!直接使用外键值obj.field_id

如果只需要一个外键值,那么使用已经在您的对象上的外键值,而不是获取整个相关对象并获取其主键。即:

entry.blog_id

替代

entry.blog.id

这里的blog对象其实并不需要(得到entry对象后再得到blog对象,最后获取其id,会导致一次多余的查询),如果后面需要blog对象,再获取也不冲突

不要排序查询结果

排序不是空闲的;order by的每个字段都是数据库必须执行的操作。如果一个模型有一个默认的顺序(Meta.ordering),并且您不需要它,那么通过调用order_by()(不带参数)在QuerySet上删除它。

向数据库中添加索引可能有助于提高排序性能。

!批量插入

在创建对象时,尽可能使用bulk_create()方法来减少SQL查询的数量。例如:

Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
])

比下面的更好

Entry.objects.create(headline='This is a test')
Entry.objects.create(headline='This is only a test')

注意,这个方法有许多注意事项,所以要确保它适合您的用例。

这也适用于ManyToManyFields,所以:

my_band.members.add(me, my_friend)

比下面的更好

my_band.members.add(me)
my_band.members.add(my_friend)

Bands 和Artists有多对多的关系。

上一篇下一篇

猜你喜欢

热点阅读