优化Django ORM查询

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

Django ORM(对象关系映射)是Django最强大的功能之一,它使我们能够通过Python代码而非SQL与数据库进行交互。

它有很多优点:

  • 数据库引擎由我们抽象,因此可以轻松切换到另一个数据库系统;

  • 支持迁移:我们可以通过更新模型来轻松地更改表,Django会自动生成更新数据库表所需的迁移脚本;

  • 支持事务:可以在一个事务中对数据库进行多次更新,如果失败,会回滚到初始状态;

但是它也有一些缺点:

  • 由于它基于SQL抽象而来,因此比较晦涩难懂,我们无法确切地知道Python代码生成哪些SQL查询;

  • Django无法得知我们何时需要使用关联表,因此无法使用JOINs

  • ORM给人一种错觉,让我们以为一些操作不会耗费什么代价。然而我们无法得知当获取一个对象属性所触发的数据库查询是否会被JOIN阻止;

为了克服这些缺点,我们需要进一步了解Django及其背后的原理。

了解机制

首先,我们需要了解系统正在发生什么,哪些SQL查询正在被执行以及哪些操作最消耗资源。

以下是检查SQL查询执行时的几种不同方法:

1. connection.queries

debug=True时,可以通过打印connection.queries来访问那些被执行的查询:

image.png

connection.queries以字典的形式保存一个SQL列表,其中包含SQL代码及其运行时间。

查询列表很容易被弄乱,为了解决这个问题,Django提供了一种方法来清理它们:

image.png

2. shell_plus -print-sql

django-extensions具有许多非常实用的特性。

shell_plus就是其中之一。如果在调用时加上-print-sql参数,将会打印代码运行时执行的SQL查询。

我将通过post演示shell_plus的执行效果,如下是一个简单的输出示例:

image.png

3. django-silk

django-silk是一个配置工具,它可以拦截请求,记录执行的SQL查询并使其可视化。

通过这个工具,您可以浏览相关请求,查看执行的SQL查询列表,还可以查看导致某个查询被执行的具体代码行。

4. django-debug-toolbar

django-debug-toolbar在浏览器上添加了一个工具栏,当浏览Django项目时,会显示许多调试信息。通过此工具,您可以查看请求中的SQL查询数量。此外,还可以进一步分析这些查询,检查SQL代码的执行顺序及每个查询消耗的时间。

优化查询

介绍示例数据库模型

我们将使用以下数据库模型作为后面各节的示例:

image.png

使用缓存的外键

如果只需访问一个ForeignKey字段的id,可以通过<field_name>_id来实现。

示例如下:

image.png

通常访问blog对象的id会通过SQL查询整个对象来实现。但是既然我们不需要其他属性,可以通过执行如下内容来避免:

image.png

让Django提前知道您的需求

对外键使用select_related

Django无法从正在查询的模型中预测何时需要ForeignKey。通过select-related工具,我们可以确切地告诉Django所需的相关模型,这样就可以执行JOINs了。

在如下示例,有一个Post模型,它属于特殊的Blog,在数据库中两者通过ForeignKey关联。

我们可以通过下面这种方式访问Post对象:

image.png

如果要通过Post来访问Blog对象,可以进行如下操作:

image.png

然而此语句会生成一个新查询从blog中获取信息,要想避免这种情况,select_related是个不错的选择。要使用它,可以将原始查询改为:

image.png

注意Django如何在SQL查询中通过JOIN来获取blog表中的属性,由于已经被缓存,因此不需要额外的查询。

image.png

select_related也可用于查询集。对于查询集,我们可以pre-select blog对象。如果不用select_relatedpre-select处理,那么假设有50Posts记录,Django在执行下述代码时会产生50次查询,相反使用select_related则只会查询一次:

image.png

对多对多字段使用prefetch_related

prefetch-relatedselect_related类似,但是它用于多对多字段。prefetch_related的工作原理不同,让我们通过一个例子来了解它。

假设我们要获取所有的Posts,并且打印每个Post的作者,可以通过如下方式实现:

image.png

image.png

注意上面的代码共产生4个查询,其中一个查询用于获取Post,其余用于获取每个Post的作者(总共3Post)。

这个就是著名的N+1问题。假设有NPosts,那么将会产生N+1次查询。在示例中,我们有3Posts,产生了4次查询。尽管目前数据不多,但是随着新Post的创建,很容易就遭遇性能瓶颈。如果有50条记录,那么将产生51次查询。

为了避免此问题,我们可以通过prefetch_relatedpre-select作者信息:

image.png

上述代码只需要2次查询,Django先获取所有的Post,然后在执行另一个SQL来获取相应的作者信息。

自定义Prefetch

在某些情况下,prefetch_related的基本语法不足以阻止Django执行额外查询,可以通过Prefetch对象来解决此问题。

示例数据库中存在PostAuthor模型,它们之间是多对多关系。假设要按作者查看其发表的全部post

image.png

image.png

如您所见,上述代码产生了3次查询,其中1个用于获取作者,另外2个用于获取每个作者发表的post

如果使用prefetch_related会怎样呢?这似乎是一个不错的决定:

image.png

image.png

发生了什么!?我们试图使用prefetch_related来减少查询次数,结果还反而多了1次。

这是由于在查询posts时,使用published=True进行过滤。Django无法使用缓存数据,因为查询时它们没有被过滤。为了避免这种情况,可以使用Prefetch对象自定义查询集:

image.png

上述代码使用Prefetch对象定义了如下内容:

  • 使用特定的查询集检索posts - 由queryset参数实现;

  • 用新属性(published_posts)存储被过滤的posts - 由to_attr参数实现;

当执行author.published_posts时,不会执行任何查询,因为相关数据已经被缓存。无论您的系统有多少个作者,只会执行2SQL查询。

总结

在使用Django ORM时,了解其运行机制非常重要。

您在本文学到的概念将有助于编写更多的优化查询,并在code review时查找可能的优化。但请注意,您需要关注优化前后的查询时间,以确保优化操作是有效的。有时更少的查询次数并不意味着更少的查询时间,类似JOINs这种操作也可能相当耗时。


英文原文:http://schegel.net/posts/optimizing-django-orm-queries/
译者:我是昵称耶~