加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

Django中的数据库访问优化——预加载关联数据

发布时间:2020-12-15 17:15:12 所属栏目:大数据 来源:网络整理
导读:Django的模型层提供了一套ORM系统,这使得我们无需学习SQL也能利用数据库来存储相关数据。一次query获取所有需要的数据,往往比多次query分别取得数据要更高效。但由于django模型的数据库检索过程隐藏在后台,不注意的话很容易导致多次检索数据库,浪费不必

Django的模型层提供了一套ORM系统,这使得我们无需学习SQL也能利用数据库来存储相关数据。一次query获取所有需要的数据,往往比多次query分别取得数据要更高效。但由于django模型的数据库检索过程隐藏在后台,不注意的话很容易导致多次检索数据库,浪费不必要的时间。因此充分理解django模型的query机制十分重要。

django官方文档给出了很多数据库访问优化的建议:Database access optimization。这些建议对于提升代码的效率十分有帮助,但其内容较多,本文只介绍其中的一种优化手段,预加载关联数据。

模型中经常会用到外键和多对多关系,但是queryset在获取对象的数据时,如果不指定的话,则不会检索关联对象的数据。当你调用关联对象时,queryset还会再一次访问数据库。因此当你循环多个对象并调用其外键所关联的对象时,django会不停的访问数据库,以获取其所需的数据。

这样说或许有点抽象,下面结合例子详细解释说明,例子使用以下的模型。

from?django.db?import?models
from?django.contrib.auth.models?import?User

class?Category(models.Model):
????name?=?models.CharField('分类',?max_length=16)

class?Topic(models.Model):
????name?=?models.CharField('话题',?max_length=16)

class?Article(models.Model):
????'文章'
????title?=?models.CharField('标题',?max_length=100)
????content?=?models.TextField('内容')
????pub_date?=?models.DateTimeField('发布日期')
????category?=?models.ForeignKey(Category)
????topics?=?models.ManyToManyField(Topic)

class?ArticleComment(BaseComment):
????'文章评论'
????content?=?models.TextField('评论')
????article?=?models.ForeignKey(Article,?related_name='comments')

先简单看看查询的机制,通过模型的 Manager构建一个QuerySet对象,QuerySet是懒加载,总是等到要用到结果时才去访问数据库。QuerySet在访问数据库时,实际上是使用SQL语句获取结果的。我们可以通过logging查看SQL语句,调整logging等级为DEBUG即可,我在另一篇文章中有介绍:如何查看Django ORM执行的sql语句。或者查看query属性print(QuerySet.query)。

>>>?Article.objects.all()
SELECT?`blog_article`.`id`,?`blog_article`.`title`,?`blog_article`.`content`,?`blog_article`.`pub_date`,?`blog_article`.`category_id`?FROM?`blog_article`;

可以看到,一般的QuerySet只取出模型对应的表中的数据,但不会取得关联表中的数据。这意味着只获得外键id,而非外键所指向的数据,至于多对多关系则什么不能获得,因为多对多关系的数据实际都保存在另一个中间表里。

select_related——预加载单个关联对象

Article与Category用外键关联,是多对一关系,一篇文章只能属于一个分类,一个分类可以包含多篇文章。获取一篇文章的分类,即调用Article.category属性。但由于文章中缓存的仅仅只是文章分类的id Article.category.id,而非完整的Category对象,所以当使用文章的category属性时,django会再次访问数据库,以检索其内容。

如下,当用for循环打印文章分类Article.category时,每一次循环都会访问一次数据库。而且,文章的分类往往是重复的,同样的分类可能在for中重复检索了多次,这样的用法显然相当耗时。

#?访问一次数据库,获得Article对象
for?a?in?Article.objects.all():
????#?访问n次数据库,每次循环都要重新检索Category的数据
????print(a.category)

使用select_related则可以一次性获取对象以及关联的对象,只需访问一次数据库:

for?a?in?Article.objects.all().select_related('category'):
????#?已经缓存了数据,不会再次访问数据库
????print(a.category)

再用query属性看一下SQL语句,select_related()使用JOIN获取了Category模型的数据。这样就预先加载了外键关联的对象,再次调用关联对象时就不会访问数据库了。

>>>?Article.objects.select_related('category')?#?all()可以省略
SELECT?`blog_article`.`id`,?`blog_article`.`category_id`,?`blog_category`.`id`,?`blog_category`.`name`?FROM?`blog_article`?INNER?JOIN?`blog_category`?ON?(`blog_article`.`category_id`?=?`blog_category`.`id`);

获取外键的外键只需用双下划线隔开就行,以此类推。比如:ArticleComment.objects.select_related('article__category')可以同时预加载该评论归属的文章以及该文章归属的分类。

然而,为了避免由于加入多个关联对象而导致的结果集太大,select_related仅限于获取单值关系——外键和一对一关系。

prefetch_related——预加载多个关联对象

预加载多个关联的对象时,需要使用prefetch_related,它分别查询每一个关系,然后在Python中完成关联对象间的连接。

接下来还是用Article与Category举例,在数据库的article表中,保存了分类的id,但是在category表中,并没有保存下属文章的id。要想获取某分类下的文章,有两种手段:

c?=?Category.objects.get(id=1)
#?这两种方法是等价的,都要访问一次数据库
c.article_set.all()
Article.objects.filter(category=c)

再看看查找多个分类下的文章时的情况:

#?访问1次数据库,获得分类
for?c?in?Category.objects.all():
????#?访问n次数据库,获得文章
????c.article_set.all()

这种情况下不能使用select_related,因为有多个关联对象时,需要用prefetch_related。这个方法会将所需的关联对象全部加载至内存中,每次调用c.article_set.all()时将直接从缓存中加载对象。

#?访问2次数据库,获得分类与文章
for?c?in?Category.objects.prefetch_related('article_set'):
????#?直接调用缓存,不再访问数据库
????c.article_set.all()

为什么是两次?第一步检索分类,第二步检索所属的文章,使用SELECT和IN语句查询,相当于:

>>>?#?prefetch_related
>>>?Category.objects.prefetch_related('article_set')
SELECT?`blog_category`.`id`,?`blog_category`.`name`,?`blog_category`.`number`?FROM?`blog_category`;
SELECT?`blog_article`.`id`,?FROM?`blog_article`?WHERE?`blog_article`.`category_id`?IN?(1,?2,?3,?...);
...
>>>?#?no?prefetch_related
>>>?c?=?Category.objects.all()
>>>?a?=?Article.objects.filter(category__in=c)
>>>?print(c)
SELECT?`blog_category`.`id`,?`blog_category`.`number`?FROM?`blog_category`;?args=()
...
>>>?print(a)
SELECT?`blog_article`.`id`,?`blog_article`.`category_id`?FROM?`blog_article`?WHERE?`blog_article`.`category_id`?IN?(SELECT?`blog_category`.`id`?FROM?`blog_category`)
...

多对多关系也是类似的情况,以Article和Topic为例,使用该方法也能预加载关联对象,Article.objects.prefetch_related('topics')。

Prefetch——进一步控制预加载操作

Prefetch可以用于进一步控制预加载时的操作,例如,下面的代码使用Prefetch将分类下的文章限制为id大于5的文章:

>>>?from?django.db.models?import?Prefetch
>>>?c=Category.objects.prefetch_related('article_set').get(id=2)
SELECT?`blog_article`.`id`,?FROM?`blog_article`?WHERE?`blog_article`.`category_id`?IN?(2);
>>>?c.article_set.count()?#?不需访问数据库
11
>>>?qs=Article.objects.filter(id__gt=5)
>>>?c=Category.objects.prefetch_related(Prefetch('article_set',queryset=qs)).get(id=2)
SELECT?`blog_article`.`id`,?FROM?`blog_article`?WHERE?(`blog_article`.`id`?>?5?AND?`blog_article`.`category_id`?IN?(2));
>>>?c.article_set.count()?#?结果与前一个不一样了
7

除此之外,还可以用to_attr参数指定预加载结果为初始对象的属性,这样就不会覆盖原来的Manager,to_attr指定的属性将预加载的结果保存在列表中。

>>>?c=Category.objects.prefetch_related(Prefetch('article_set',queryset=qs,to_attr='aidgt5')).get(id=2)
SELECT?`blog_article`.`id`,?FROM?`blog_article`?WHERE?(`blog_article`.`id`?>?5?AND?`blog_article`.`category_id`?IN?(2));
>>>?c.article_set.count()?#?执行SQL语句,因为没有缓存该queryset
SELECT?COUNT(*)?AS?`__count`?FROM?`blog_article`?WHERE?`blog_article`.`category_id`?=?2;
11
>>>?len(c.aidgt5)?#?已缓存,无需访问数据库
7

预加载性能对比

使用select_related和prefetch_related能大大减少访问数据库的次数,但这对性能有多大提升呢?我们依然没有一个直观上的印象。接下来将通过实际运行代码,对比非预加载和预加载在效率上的区别。(其实是因为我不会分析算法复杂度,只能对比实际运行时间了...)

def?articles_retrieve_no_prefetch():
????for?a?in?Article.objects.all():
????????print(a.category,'n',a.topics.all())

def?articles_retrieve_prefetch():
????for?a?in?Article.objects.all()?
????.select_related('category')?
????.prefetch_related('topics'):
????????print(a.category,a.topics.all())

上面两个函数分别定义了简单的数据库查询,以及使用了预加载的数据库查询,并打印其结果。

测试方法为两个函数分别运行100次,并统计单次运行所耗费的时间。具体的统计函数将放在后面,硬件和软件配置就不赘述了,直接上结果,其中平均值为函数单次运行所花费的时间,单位为s。

很明显,预加载的性能更高。

1.jpg

django的模型虽然简单易用,但是不能浅尝辄止,要深入理解其背后的原理,并合理使用查询方法。否则很容易执行许多次不必要的数据库访问,造成严重的性能浪费。

因此,数据库访问优化至关重要,本文仅仅只是介绍了一种优化方法,更多的优化方法请参考django官方文档:Database access optimization。

下面给出详细的统计函数,如果有兴趣可以在自己的项目中跑一下,

'''run?this?uder?Django?project'''
import?time,?statistics

from?django.utils?import?timezone
from?blog.models?import?Article

def?count_run_time(func):
????start=time.perf_counter()
????func()
????end=time.perf_counter()
????return?end-start

def?statistic_run_time(func,?n):
????data?=?[?count_run_time(func)?for?i?in?range(n)]
????mean?=?statistics.mean(data)
????sd?=?statistics.stdev(data,?xbar=mean)
????return?[data,?mean,?sd,?max(data),?min(data)]

def?compare_articles_retrieve_time(n):
????result1?=?statistic_run_time(articles_retrieve_no_prefetch,?n)
????result2?=?statistic_run_time(articles_retrieve_prefetch,?n)
????print('对比t?no?prefetcht?prefetch')
????print('平均值t',result1[1],'t',result2[1])
????print('标准差t',result1[2],result2[2])
????print('最大t',result1[3],result2[3])
????print('最小t',result1[4],result2[4])

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读