当前位置: 首页 > news >正文

Django 数据库查询优化

对于网站和Web APP来说最影响网站性能的就是数据库查询了,因为反复从数据库读写数据很耗时间和计算资源,而查询返回的数据集非常大时还会占据很多内存。这里从django orm的角度来探索数据库查询的优化。

QuerySet与查询

什么是QuerySet

QuerySet是Django提供的强大的数据库接口(API)。正是因为通过它,我们可以使用filter, exclude, get等方法进行数据库查询,而不需要使用原始的SQL语言与数据库进行交互。从数据库中查询出来的结果一般是一个集合,这个集合叫就做 queryset。

QuerySet是惰性的

当我们使用如filter语句获得queryset,Django的数据接口QuerySet并没有对数据库进行查询,只有在做进一步运算时(如打印查询结果、判断是否存在、计算结果长度)才会执行真正的数据库查询,这个过程就是queryset的执行(evaluation)。这样做的目的是减少对数据库无效的操作。

QeurySet自带缓存

当queryset被执行后,其查询结果会载入内存并保存在queryset内置的cache中。再次使用就不需要重新去查询了

判断查询结果是否存在

ifexists()都可以判断查询结果是否存在,但两者使用却又很大的不相同。if会触发整个queryset的缓存,而exists()只会返回TrueFalse检视查询结果是否存在而不会缓存查询结果。选用哪个办法来判断需要根据实际使用需求来看。

统计查询结果数量

len()count()方法均能统计查询结果数量,这里也不说哪个更好。count()是从数据库层面直接获取查询结果数量而不需要返回整个queryset数据集一般来说会更快。len()会导致queryset的执行,需要先将整个数据集载入内存方可计算,但如果queryset数据集已经缓存在内存当中了len()则会更快

按需获取数据

当查询到的queryset非常大时,会占用大量的内存,使用valuesvalues_list按需提取数据

注意: values和values_list返回的是字典形式字符串数据,而不是对象集合

使用update更新数据

相比于使用save()方法,update()不需要先缓存整个queryset

使用explain方法分析耗时,优化查询

Blog.objects.filter(title='My Blog').explain(verbose=True)

# output
Seq Scan on public.blog  (cost=0.00..35.50 rows=10 width=12) (actual time=0.004..0.004 rows=10 loops=1)
  Output: id, title
  Filter: (blog.title = 'My Blog'::bpchar)
Planning time: 0.064 ms
Execution time: 0.058 ms

aggregate和annotate

Django的aggregateannotate方法主要用于组合查询,当我们需要对查询集(queryset)的某些字段进行计算,或先分组在计算或排序就需要用到aggregateannotate方法了

准备工作

一个模型StudentHobbit是多对多关系:

class Student(models.Model):

    name = models.CharField(max_length=20)
    age = models.IntegerField()
    hobbies = models.ManyToManyField(Hobby)
    
class Hobby(models.Model):
    name = models.CharField(max_length=20)

aggregate

aggregate方法支持的聚合操作有MINMAXAVGSUMCOUNT,所以先提前import进来:

from django.db.models import Max, Min, Avg, Sum, Count

一些例子:

# 计算学生平均年龄, 返回字典。age和avg间是双下划线哦
Student.objects.all().aggregate(Avg('age'))
{ 'age__avg': 12 }

# 学生平均年龄,返回字典。all()不是必须的。
Student.objects.aggregate(Avg('age'))
{ 'age__avg: 12' }

# 计算学生总年龄, 返回字典。
Student.objects.aggregate(Sum('age'))
{ 'age__sum': 144 }

# 学生平均年龄, 设置字典的key
Student.objects.aggregate(average_age = Avg('age'))
{ 'average_age': 12 }

# 学生最大年龄,返回字典
Student.objects.aggregate(Max('age'))
{ 'age__max': 12 }

# 同时获取学生年龄均值, 最大值和最小值, 返回字典
Student.objects.aggregate(Avg('age‘), Max('age‘), Min('age‘))
{ 'age__avg': 12, 'age__max': 18, 'age__min': 6, }

# 根据Hobby反查学生最大年龄。查询字段student和age间有双下划线哦。
Hobby.objects.aggregate(Max('student__age'))
{ 'student__age__max': 12 }

annotate

annotate的中文意思是注释,但似乎有点词不达意,更好的理解应该是分组(group by)。如果需要对数据集先进行分组然后再进行某些聚合操作或排序时,就需要用annotate来实现。与aggregate不同的是annotate返回的是一个查询集,该查询集相当于是在原来的基础上多加了一个统计字段

一些例子:

# 按学生分组,统计每个学生的爱好数量
Student.objects.annotate(Count('hobbies'))

# 按学生分组,统计每个学生爱好数量,并自定义字段名
Student.objects.annotate(hobby_count_by_student=Count('hobbies'))

# 按爱好分组,再统计每组学生数量。
Hobby.objects.annotate(Count('student'))

# 按爱好分组,再统计每组学生最大年龄。
Hobby.objects.annotate(Max('student__age'))

annotate与filter联用

有时我们需要对数据集先筛选再分组或先分组再筛选,就可以通过annotate与filter联用来实现

一些例子:

# 先按爱好分组,再统计每组学生数量, 然后筛选出学生数量大于1的爱好。
Hobby.objects.annotate(student_num=Count('student')).filter(student_num__gt=1)

# 先按爱好分组,筛选出以'd'开头的爱好,再统计每组学生数量。
Hobby.objects.filter(name__startswith="d").annotate(student_num=Count('student‘))

annotate与order_by联用

一些例子:

# 先按爱好分组,再统计每组学生数量, 然后按每组学生数量大小对爱好排序。
Hobby.objects.annotate(student_num=Count('student‘)).order_by('student_num')

# 统计最受学生欢迎的5个爱好。
Hobby.objects.annotate(student_num=Count('student‘)).order_by('-student_num')[:5]

annotate与values联用

在前面的例子中分组都是按照对象分组的,如按学生对象分组,同样的也可以通过values按如学生姓名name来分组,如果两个学生具有相同的名字他们的爱好将叠加

# 按学生名字分组,统计每个学生的爱好数量。
Student.objects.values('name').annotate(Count('hobbies'))

你还可以使用values方法从annotate返回的数据集里提取你所需要的字段,如下所示:
# 按学生名字分组,统计每个学生的爱好数量。
Student.objects.annotate(hobby_count=Count('hobbies')).values('name', 'hobby_count')

select_related和prefetch_related

准备工作

文章(Article)与类别(Category)是一对多关系,文章(Article)与标签(Tag)是多对多关系

class Article(models.Model):
    """文章模型"""
    title = models.CharField('标题', max_length=200, db_index=True)
    category = models.ForeignKey('Category', verbose_name='分类', on_delete=models.CASCADE, blank=False, null=False)
    tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True)

糟糕的用法

articles = Article.objects.all()
for article in articles:
  print(article.title)
  print(article.category.name)
  for tag in article.tags.all():
    print(tag.name)

当使用Article.objects.all()查询得到的只是Article表的数据,并没有包含Category表和Tag表的数据。因此每一次打印article.category.nametag.name都会重新去查询一遍Category表和Tag表,造成了很大不必要的浪费

select_related

select_related会根据外键关系(仅限一对一、一对多),使用inner join来一次性获取主体对象和相关对象的信息,这样在打印article.category.name时就不用去重新查询数据库了

修改article查询语句:

articles = Article.objects.all().select_related('category')

selected_related常用使用案例:

# 获取id=13的文章对象同时,获取其相关category信息
Article.objects.select_related('category').get(id=13)

# 获取id=13的文章对象同时,获取其相关作者名字信息
Article.objects.select_related('author__name').get(id=13)

# 获取id=13的文章对象同时,获取其相关category和相关作者名字信息。下面方法等同
Article.objects.select_related('category', 'author__name').get(id=13)
Article.objects.select_related('category').select_related('author__name').get(id=13)

# 使用select_related()可返回所有相关主键信息,all()非必需
Article.objects.all().select_related()

# 获取Article信息同时获取blog信息,filter方法和selected_related方法顺序不重要
Article.objects.filter(pub_date__gt=timezone.now()).select_related('blog')
Article.objects.select_related('blog').filter(pub_date__gt=timezone.now())

prefetch_related

在多对多关系中不能再使用selectd_related,因为多对多JOIN操作后表会变得非常的大。而prefetch_related就是用来处理这个问题的,prefect_related可用于多对多关系字段,也可用于反向外键关系(related_name)

再次修改article的查询语句:

articles = Article.objects.all().select_related('category').prefecth_related('tags')

prefetch_related常用使用案例:

# 文章列表及每篇文章的tags对象名字信息
Article.objects.all().prefetch_related('tags__name')

# 获取id=13的文章对象同时,获取其相关tags信息
Article.objects.prefetch_related('tags').get(id=13)

用Prefetch方法可以给prefetch_related方法额外添加额外条件和属性
# 获取文章列表及每篇文章相关的名字以P开头的tags对象信息
Article.objects.all().prefetch_related(
    Prefetch('tags', queryset=Tag.objects.filter(name__startswith="P"))
)

# 文章列表及每篇文章的名字以P开头的tags对象信息, 放在article_p_tag列表
Article.objects.all().prefetch_related(
    Prefetch('tags', queryset=Tag.objects.filter(name__startswith="P")), to_attr='article_p_tag'
)

F()函数

F()会生成数据库级别的SQL表达式,直接使用它引用模型字段的值并执行数据库操作,而不用把它导出到Python的内存当中

对比两个例子:

1
article = Article.objects.get(title='文章2')
article.thumb_count += 1
article.save()2 使用F()函数
Article.objects.filter(title='文章1').update(thumb_count=F('thumb_count')+1)

很明显使用F()函数的执行效率会更高,只需要一条sql完全的数据库操作,而例1则需要先查询,缓存,然后再更新

F()函数避免竞争

在Python线程中例1的方法是存在竞态条件的,如第一个线程完成取值、更新值、保存新值,而第二个线程操作还是使用就的值来进行操作,使用F()函数的话,因为是数据库层面的原子操作,第二个线程再来取值那也是取到更新后的值了

F()函数用于查询表达式

例如:

同一数据不同字段比较
article = Article.objects.filter(thumb_count__gt=F('view_count'))

两个操作数都是常数和F()函数的加、减、乘、除、取模、幂计算等算术操作
article = Article.objects.filter(view_count__gt=F('thumb_count') * 2)

配合annotate使用
article = Article.objects.annotate(all_count=F('view_count') + F('thumb_count'))

优化tips

  1. 利用 queryset lazy 的特性 去优化代码,尽可能的减少连接数据库的次数
  2. 如果查出的 queryset 只用一次,可以使用 iterator() 去来防止占用太多的内存
  3. 尽可能把一些数据库层级的工作放到数据库,例如使用 filter/exclude, F, annotate, aggregate
  4. 一次性拿出所有你要的数据,不去取那些你不需要的数据
  5. 意思就是要巧用select_related()、prefetch_related()和values_list()、values ()例如如果只需要id字段的话,用values_list(‘id’, flat=True)也能节约很多资源。或者使用defer()和only()方法:不加载某个字段(用到这个方法就要反思表设计的问题了)只加载某些字段.
  6. 如果不用select_related的话,去取外键的属性就会连数据再去查找.
  7. bulk(批量)地去操作数据,比如bulk_create
  8. 查找一条数据时,尽量用有索引的字段去查询,O(1)或O(log n)和O(n)差别还是很大的
  9. 用count()代替len(queryset), 用exists()代替if queryset:

[参考]

QuerySet特性及高级使用技巧
aggregate和annotate方法使用详解与示例
select_related和prefetch_related的用法与区别

相关文章:

  • pycharm创建vue项目
  • Vue的路由配置,模块引入
  • CSS学习:设置全局背景图
  • css学习:透明的设置
  • element学习:form表单的校验
  • 配置Message全局弹窗
  • Vue学习:存入会话缓存
  • vue实现简单的记住密码功能
  • element设置表格el-table表头的颜色
  • vue+django跨域问题的学习
  • django清空数据库
  • 将json字符串转换成json对象
  • 小程序节流,防止多次点击
  • django models进行倒序
  • 微信小程序的动态显示字体颜色
  • 3.7、@ResponseBody 和 @RestController
  • ECMAScript入门(七)--Module语法
  • gf框架之分页模块(五) - 自定义分页
  • Git的一些常用操作
  • Git同步原始仓库到Fork仓库中
  • JavaScript类型识别
  • Java精华积累:初学者都应该搞懂的问题
  • js中的正则表达式入门
  • MD5加密原理解析及OC版原理实现
  • Rancher如何对接Ceph-RBD块存储
  • Redux 中间件分析
  • 从@property说起(二)当我们写下@property (nonatomic, weak) id obj时,我们究竟写了什么...
  • 技术发展面试
  • 开放才能进步!Angular和Wijmo一起走过的日子
  • 如何编写一个可升级的智能合约
  • 手写一个CommonJS打包工具(一)
  • 云大使推广中的常见热门问题
  • 阿里云移动端播放器高级功能介绍
  • #微信小程序:微信小程序常见的配置传值
  • (1)安装hadoop之虚拟机准备(配置IP与主机名)
  • (2.2w字)前端单元测试之Jest详解篇
  • (2020)Java后端开发----(面试题和笔试题)
  • (Matalb时序预测)WOA-BP鲸鱼算法优化BP神经网络的多维时序回归预测
  • (Python第六天)文件处理
  • (亲测)设​置​m​y​e​c​l​i​p​s​e​打​开​默​认​工​作​空​间...
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • (四)Controller接口控制器详解(三)
  • (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境
  • (转) 深度模型优化性能 调参
  • (转)h264中avc和flv数据的解析
  • (转)http-server应用
  • (轉貼) VS2005 快捷键 (初級) (.NET) (Visual Studio)
  • .NET CF命令行调试器MDbg入门(二) 设备模拟器
  • .net core 连接数据库,通过数据库生成Modell
  • .NET(C#) Internals: as a developer, .net framework in my eyes
  • .NET使用存储过程实现对数据库的增删改查
  • .Net中间语言BeforeFieldInit
  • [Angular] 笔记 9:list/detail 页面以及@Output
  • [C++从入门到精通] 14.虚函数、纯虚函数和虚析构(virtual)
  • [docker] Docker容器服务更新与发现之consul