性能优化
是程序开发中必不可少的环节。理论上,一开始程序员就应该写
性能最优
的代码。现实中受限于经验、项目进度等因素制约,总有一些问题在暴露后方能解决。
本次复盘仅针对
查询
,涉及到:
- 减少不必要的
IO
(只加载有需要的字段及用时才加载) - 消灭查询
N+1
- 减少代码层面的运算
1、减少不必要的
IO
:延迟查询
defer
defer
的宗旨是:用的时候才加载,下面是一个简单的博客列表页示例:
models.py
classBlog(models.Model):
title = models.CharField()
content = models.TextField()
is_special = BoolenField(default=False)
views.py
# 使用简单演示 实际上需要分页classBlogListView(View):defget(self, request):
blogs = Blog.objects.defer('content')return render(request,'list.html')
list.html
{% for blog in blogs %}
<!--需要时才读取content的内容-->
{% if blog.is_special %}
{{ blog.content }}
{% endif %}
{{ blog.title }}
{% endfor %}
list.html
是一个博客列表页,只显示标题列表,
blogs = Blog.objects.defer('content')
,声明了整个查询集不会立刻读取
content
字段,只读取
title
和
is_special
的字段内容,需要时才读取某条记录的
content
内容。
相比之下,如果修改视图
.all()
,会发生什么事
blogs = Blog.objects.all()
当访问到达时,会把所有的文章内容都读取并放到内存中,实际上
content
字段我们是用不到的,或者有有条件地用。这无疑增加了服务器的开销,让用户多等了一会。即便是在分页的情况下,也不应这么写。
defer
的特点是,被声明的字段不会加载,用到某条记录的
content
,也仅仅是加载该记录的
content
,极大地减少了数据库的
IO
和服务器的内存开销,提高了响应速度。
2、减少不必要的
IO
:
only
有限查询
defer
和
only
是互补的,依然是上面的示例和需求:
views.py
# 使用简单演示 实际上需要分页classBlogListView(View):defget(self, request):
blogs = Blog.objects.only('title')return render(request,'list.html')
它只立刻读取
title
字段,其他的字段会使用延迟加载的策略,即除了
title
,其他的都用时才加载。
不管是
defer
和
only
,返回的始终是
QuerySet
,在
filter
后使用
defer
或
only
也是可以的,如:
blogs = Blog.objects.filter(is_special=True).only('title')
3、减少代码层面的运算:
values
/
values_list
依然使用上面的示例,现在前端要通过
js
发起的博客列表的请求并渲染到页面中,通常的做法是:
views.py
classBlogListView(View):defget(self, request):
blogs = Blog.objects.only('title')
data =[]for blog in blogs:
data.append({'title': blog.title})return JsonResponse({'data': data})
这么做不会有错,不过有更好的方式可以实现,可以省去
python
代码层面的开销,提高性能:
classBlogListView(View):defget(self, request):
blogs = Blog.objects.values('title')return JsonResponse({'data':list(blogs)})
values
只会读取指定字段,返回一个包含类字典的
QuerySet
,如:
<QuerySet [{'name':'春天的故事'}, {'name':'Python入门与精通'}, {'name':'java企业级实战项目'}}]>
查询集是一个
可迭代对象
,使用
list
即可转换成字典列表响应给前端。
values_list
作用类似,单个元素是一个只包含指定字段的值的元组。
4、消灭查询
N+1
:跨关系查询
select_related
把上面的
Blog
模型再丰富一下,增加作者的模型,并添加外键关系:
models.py
classAuthor(models.Model):
author = models.CharField()classBlog(models.Model):
title = models.CharField()
content = models.TextField()
is_special = BoolenField(default=False)
author = models.ForeignKeyField(Author, models.CASCADE)
在博客列表页中,除了显示博客标题外,还需要显示博客的作者,示例:
- 不正确的做法:
views.py
classBlogListView(View):defget(self, request):
blogs = Blog.objects.all()
data =[]for blog in blogs:# for 循环中,blog.author.author反复读取Author表,如果作者相同会造成重复读取同一条记录
data.append({'title': blog.title,'author': blog.author.author})return JsonResponse({'data': data})
入门
django
时,经常踩这个坑,后来意识到会造成反复读取同一条数据时做了功夫,我记得大概是这样的:
classBlogListView(View):defget(self, request):
blogs = Blog.objects.all()
authors =[]
data =[]
author_index =0for blog in blogs:
author =None# for 循环中,blog.author.author反复读取Author表,如果作者相同会造成重复读取同一条记录if blog.author notin authors:
authors.append(blog.author)
author = blog.author.author
author_index +=1else:
author = authors[authors.index(blog.author)].author
data.append({'title': blog.title,'author': author})return JsonResponse({'data': data})
虽然没有
N+1
的问题了,但多了循环和条件分支,增加了代码运算的开销。
django
有便捷的查询接口,通常为:
classBlogListView(View):defget(self, request):# 一次性将author表相关记录读取出来
blogs = Blog.objects.select_related('author').all()
data =[]for blog in blogs:# for 循环中,不会反复读取数据库
data.append({'title': blog.title,'author': blog.author.author})return JsonResponse({'data': data})
select_related('author')
使得执行
sql
时会通过一次查询,把涉及到的所有外键
Author
记录全部拿出来放到内存中,避免在循环时反复向数据库,导致增加额外的
IO
,这就是常说的避免查询时
N+1
的问题,其中
1
是我们想要的查询,但产生了
N
次额外的不必要的查询。
注:
1、因为要预取数据,所以
select_related
和
defer/only
是天生相克的,并不能放到一起使用;
2、
select_related
适用于
外键和一对一
的简单查询,它也可以不指定外键,置空时会提前读取所有外键的所关联的数据,本质上,
select_related
是将表达式构建成一个完整的
sql
表达式并在数据库层面进行查询,这一点和下面要说的
prefetch_related
稍有不同。
5、跨表查询
prefetch_related
消灭
N+1
该方法通常用于跨
多对多
查询(其实外键也可以),为方便演示,再次扩展模型,需求和上面的一样:
classAuthor(models.Model):
author = models.CharField()classTags(models.Model):
tag = models.CharField()classBlog(models.Model):
title = models.CharField()
content = models.TextField()
is_special = BoolenField(default=False)
author = models.ForeignKeyField(Author, models.CASCADE)
tags = models.ManyToManyField(Tags)
查询时产生典型的
N+1
问题如下:
blogs = Blog.objects.all()for blog in blogs:
tags =[]
tags.append(blog.tags.all())print(f'{blog}的标签有{tags}')
遍历过程中,执行
blog.tags.all()
的后续会产生大量的查询,每一篇博客的标签都有可能相同,而每一次都要执行查询,浪费了资源。
推荐用法:
blogs = Blog.objects.prefetch_related('tags')
prefetch_related
返回的依然是
QuerySet
,而且,它还支持
JOIN
语法,为此,做增加一个模型并和
Tags
模型关联:
classColor(models.Model):
color = models.CharField()classTags(models.Model):
tag = models.CharField()
colors = models.ManyToManyField()
blogs = Blog.objects.prefetch_related('tags__color')
在上面的
prefetch_related
中,不仅一次查询了
Tags
相关记录,而且还查询了
Color
的相关记录,实现了一次查询到两个表的相关记录。
如果不使用
django
提供的这些接口来进行查询,要想达到相同的效果,
python
层面的代码量会成倍增加,况且
python
代码的执行要逊于数据库层面的操作。
注
1、
prefetch_related
能和
defer/only
一起使用,也能和
select_related
一起使用,以获得更佳的性能;
2、
prefetch_related
也能用于外键查询。
6、跨表查询
Prefetch
消灭
N+1
Prefetch
是一个类,它结合
prefetch_related
能实现更精细的查询控制,以获取更优的查询性能,
示例一:
需求:获取所有
Author
的同时,查询每个
Author
的
Blog
,将结果保存到一个变量中并将该变量变成
Author
实例的一个属性:
from django.db.models import Prefetch
authors = Author.objects.prefetch_related(
Prefetch('blog_set', queryset=Blog.objects.only('id','title'), to_attr='blogs'))for author in authors:# 已预加载print(author.name)# 已预加载 额外的属性print(author.blogs)
Prefetch
实例能为
prefetch_related
提供更为精细的查询控制,参数含义:
blog_set
在查询Author
时,同时去查询关联表Blog
queryset
为查询Blog
时提供更多的条件控制,比如使用filter=(status=True)
to_attr
为每个Author
查询到Blog
记录后将数据保存到临时变量blogs
中并绑定到该author
实例中
示例二(使用新的模型):
classAuthor(models.Model):
name = models.CharField(max_length=100)classPublisher(models.Model):
name = models.CharField(max_length=300)classBook(models.Model):
name = models.CharField(max_length=300)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
需求:查询所有书籍的同时,预加载出版社信息,并预加载每本书的所有关联作者:
books = Book.objects.select_related('publisher').prefetch_related(
Prefetch('author', queryset=Author.objects.only('name','id')))for book in books:# 已预加载 不会有N+1的问题print(book.publisher)# 已预加载 不会有N+1的问题print(book.authors)
当然了,下面的方法也能实现:
books = Book.objects.select_related('publisher').prefetch_related('authors')
不过,缺点也明显,
prefetch_related
无法对
Author
的查询集做延迟加载,会加载不需要的字段,故此,最上面的查询语句可以控制得精准:
books = Book.objects\
# 改用prefetch_related实现.prefetch_related(Prefetch('publisher', queryset=Publisher.objects.only('id','name')))\
.prefetch_related(Prefetch('authors', queryset=Author.objects.only('name','id')))
7、使用
annotate
聚合函数减少代码层面的运算
在第6点示例中:
authors = Author.objects.prefetch_related(
Prefetch('blog_set', queryset=Blog.objects.only('id','title'), to_attr='blogs'))
其中
to_attr
能为示例绑定新的属性,使用
annotate
也能为查询集中的每个实例绑定新的属性,它是基于数据库运算的结果,性能要比在代码层面去实现要好,
示例需求:获取所有作者,并为每个作者实例统计其书本的数量:
authors = Author.objects.annotate(book_count=Count('book'))for author in authors:# 当前作者书本的总量print(author.book_count)
示例需求:获取所有作者,并为每个作者实例统计其书本涉及到的出版社的数量:
authors = Author.objects.annotate(publisher_count=Count('book__publisher'))for author in authors:# 当前作者书本所涉及到出版社的总量print(author.publisher_count)
注:
聚合函数的用法远不止于此,能使用聚合函数实现的,就不要使用自己的代码来实现,因为聚合函数是运行在数据库层面,性能高于自己使用代码实现的逻辑。
业务环境中的查询远比这里的示例要复杂,尤其是与数据统计相关的查询,只有多使用,多理解多优化才能变得随手拈来。
总结
1、总是优先使用
django
提供的查询接口,实现复杂的查询需求;
2、日常使用中,要让消灭
N+1
查询成为习惯,而不是成为优化性能的手段;
3、使用聚合函数来替代一些可替代的运算场景以减少开销提升效率。
版权归原作者 野生码农一灯 所有, 如有侵权,请联系我们删除。