Django 使用select_related和prefetch_related
Django ORM代表对象关系映射器,用于与数据库进行交互,我们可以以Pythonic的方式存储数据。本教程将详细解释Django ORM查询优化中的一个重要概念 – select_related和prefetch_related。
什么是Django中的select_related
当我们从数据库中获取任何对象时,它返回一个新的queryset,该queryset由该对象而不是特定对象组成。它将在访问时创建所有相关对象的新查询集。但这并不适用于所有情况。
在执行select_related操作之前,我们应该在setting.py文件中添加以下日志记录,以查看Django CRM中运行的所有SQL查询。
将以下代码片段与settings.py中的LOGGING字段合并:
LOGGING = {
'version': 1,
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
}
},
'handlers': {
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
}
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
}
}
}
Model.py
我们创建了两个模型,一个是电影(Movie)和导演(Director)。电影模型有三个字段,包括 电影标题(movie_title)、 发布年份(release_year)、 和 导演外键(director foreign key) 。
from django.db import models
class Director(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class Movie(models.Model):
movie_title = models.CharField(max_length=150)
release_year = models.IntegerField()
director = models.ForeignKey(Director, on_delete = models.CASCADE, max_length=100)
def __str__(self):
return self.name
现在我们将运行以下命令来创建数据库中的这些表格。
python manage.py makemigrations
python manage.py migrate
我们将打开Django shell,并创建对象以将日期存储在表中。
python manage.py shell
在Django shell中创建导演对象。
>>> from sampleapp.models import Director, Movie
>>> director1 = Director.objects.create(name = "Steven Spiellberg")
>>> director1.save()
>>> director2 = Director.objects.create(name = "Christopher Nolan")
>>> director2.save()
>>> director3 = Director.objects.create(name = "Alan Taylor")
>>> director3.save()
我们使用以下命令来获取导演的查询集。
>>> Director.objects.all()
(0.000) SELECT "sampleapp_director"."id", "sampleapp_director"."name" FROM "sampleapp_director" LIMIT 21; args=()
, , ]>
我们可以看到内部运行的相应SQL查询。
现在我们将创建电影模型的对象。
movie1 = Movie.objects.create(name = "Super 8", release_year = '2011', director = director1)
(0.141) INSERT INTO "sampleapp_movie" ("movie_title", "release_year", "director_id") VALUES ('Super 8', 2011, 1); args=['Super 8', 2011, 1]
>>> movie1.save()
(0.172) UPDATE "sampleapp_movie" SET "movie_title" = 'Super 8', "release_year" = 2011, "director_id" = 1 WHERE "sampleapp_movie"."id" = 1; args=('Super 8', 2011, 1, 1)
上面的输出显示了电影对象的SQL查询。与我们创建的三个电影对象相同。
>>> from sampleapp.models import Director, Movie
>>> Movie.objects.all()
(0.000) SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_movie"."release_year", "sampleapp_movie"."director_id" FROM "sampleapp_movie" LIMIT 21; args=()
, , , , , , ]>
电影表与导演表存在外键关系。因此,我们可以使用以下查询语句来获取数据。
>>> dir = Movie.objects.get(id = 2)
(0.016) SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_movie"."release_year", "sampleapp_movie"."director_id" FROM "sampleapp_movie" WHERE "sampleapp_movie"."id" = 2 LIMIT 21; args=(2,)
>>> dir.director.name
(0.000) SELECT "sampleapp_director"."id", "sampleapp_director"."name" FROM "sampleapp_director" WHERE "sampleapp_director"."id" = 2 LIMIT 21; args=(2,)
'Christopher Nolan'
如上面的代码所示,我们需要运行一个单独的查询来获取电影对象的导演名称。 这些针对相关对象的单独查询会降低应用程序的性能。假设我们有1000部电影,我们需要创建一份包含作者姓名的电影列表。 每次访问外键时,我们都需要进行另一个查询来检索值。因此,我们最终会运行1001个查询来获取图书列表。 为了解决这个问题,Django提供了 ,它可以将1001个查询减少到1个。 选择关联 在表中执行内连接操作。在下面的示例中,movie_id与director.id进行了内连接。 示例-
>>> movie = Movie.objects.select_related('director').all()
让我们创建一个查询,一次性获取所有名字为导演的电影。
打开Django shell并输入以下查询。
>>>movies=Movie.objects.select_related('director').annotate(name=F('director__name')).values('id', 'movie_title', 'name')
>>> movies
(0.000) SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_director"."name" AS "name" FROM "sampleapp_movie" INNER JOIN "sampleapp_director" ON ("sampleapp_movie"."director_id" = "sampleapp_director"."id") LIMIT 21; args=()
在输出中,我们可以看到只调用了一个连接查询来获取所有与关联导演有关的电影。这对于应用程序来说是一个重大改进。
select_related() 和 all() 之间的区别
- 没有 select_related
>>> Movie.objects.select_related('director').all()
(0.0) SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_movie"."release_year", "sampleapp_movie"."director_id", "sampleapp_director"."id", "sampleapp_director"."name" FROM "sampleapp_movie" INNER JOIN "sampleapp_director" ON ("sampleapp_movie"."director_id" = "sampleapp_director"."id") LIMIT 21; args=()
(1.0)
, , , , , , , ]>
- 使用select_related
现在我们将使用all()方法运行查询。
from sampleapp.models import Director, Movie
>>> Movie.objects.all()
(0.000)
SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_movie"."release_year", "sampleapp_movie"."director_id" FROM "sampleapp_movie" LIMIT 21; args=()
, , , , , , , ]>
我们从这两个查询中得到了相同的数据,但查询检索方式存在差异。
现在让我们看看另一种情况,我们想要获取第一部电影的导演姓名。
- 没有使用 select_related
>>> Movie.objects.all()[0].director
(0.000) SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_movie"."release_year", "sampleapp_movie"."director_id" FROM "sampleapp_movie" LIMIT 1; args=()
(0.000) SELECT "sampleapp_director"."id", "sampleapp_director"."name" FROM "sampleapp_director" WHERE "sampleapp_director"."id" = 1 LIMIT 21; args=(1,)
我们可以观察到有两个SQL查询用于获取导演的名称。第一个查询获取电影名称,第二个查询获取关联的导演名称。这可能会在应用程序中造成很多冗余。让我们看看如何使用 单个查询 来完成相同的工作。
- 使用select_related
>>> Movie.objects.select_related('director').all()[0].director
(0.000) SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_movie"."release_year", "sampleapp_movie"."director_id", "sampleapp_director"."id", "sampleapp_director"."name" FROM "sampleapp_movie" INNER JOIN "sampleapp_director" ON ("sampleapp_movie"."director_id" = "sampleapp_director"."id") LIMIT 1; args=()
select_related 仅限于外键关系。如果存在多对多关系,则无法使用 select_related。在这种情况下,我们可以使用 prefech_related 。
Prefetch Related
通过减少查询数量,可以使用 prefetch_related 来改善多对多关系的性能。让我们来了解以下示例。
>>> movies = Movie.objects.prefetch_related('publisher')
>>> movies
(0.000) SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_movie"."release_year", "sampleapp_movie"."director_id" FROM "sampleapp_movie" LIMIT 21; args=()
(0.000) SELECT ("sampleapp_movie_publisher"."movie_id") AS "_prefetch_related_val_movie_id", "sampleapp_director"."id", "sampleapp_director"."name" FROM "sampleapp_director" INNER JOIN "sampleapp_movie_publisher" ON ("sampleapp_director"."id" = "sampleapp_movie_publisher"."director_id") WHERE "sampleapp_movie_publisher"."movie_id" IN (1, 2, 3, 4, 5, 6, 7, 8); args=(1, 2, 3, 4, 5, 6, 7, 8)
<QuerySet [<Movie: Super 8>, <Movie: Ready Player One>, <Movie: Munich>, <Movie: The Terminal>, <Movie: Game of Thrones>, <Movie: The Terminal>, <Movie: Super 8>, <Movie: Udan>]>
当我们尝试使用发布商获取电影时,它在后台运行两个SQL查询
movie = Movie.objects.prefetch_related('publisher').all()[0].publisher
(0.000) SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_movie"."release_year", "sampleapp_movie"."director_id" FROM "sampleapp_movie" LIMIT 1; args=()
(0.000) SELECT ("sampleapp_movie_publisher"."movie_id") AS "_prefetch_related_val_movie_id", "sampleapp_director"."id", "sampleapp_director"."name" FROM "sampleapp_director" INNER JOIN "sampleapp_movie_publisher" ON ("sampleapp_director"."id" = "sampleapp_movie_publisher"."director_id") WHERE "sampleapp_movie_publisher"."movie_id" IN (1); args=(1,)
我们可以使用 prefetch_related() 来解决这个问题。让我们来理解下面的示例。
>>> movie = Movie.objects.prefetch_related('publisher').values('id', 'movie_title', 'publisher')
>>> movie
(0.000) SELECT "sampleapp_movie"."id", "sampleapp_movie"."movie_title", "sampleapp_movie_publisher"."director_id" FROM "sampleapp_movie" LEFT OUTER JOIN "sampleapp_movie_publisher" ON ("sampleapp_movie"."id" = "sampleapp_movie_publisher"."movie_id") LIMIT 21; args=()
结论
到目前为止,我们已经看到了如何在Django中使用 select_related 和 prefetch_related 来高效地减少查询的开销。select_related主要用于外键关联,它通过与关联表进行INNER JOIN操作来获取数据。
另一方面, prefetch_related 用于处理多对多关系。它通过多表关联查询同时获取所有数据,并通过减少数据库查询来提高性能。
但是在处理多对多关系时,使用JOIN操作并不是一个明智的选择,因为这将是一个非常冗长和繁琐的过程。这样做会导致非常耗时且SQL语句繁琐。为了解决这个问题,我们将使用 prefetch_related 。