Django笔记二十八之数据库查询优化汇总
本文首发于公众号:Hunter后端
原文链接:Django笔记二十八之数据库查询优化汇总
这一篇笔记将从以下几个方面来介绍 Django 在查询过程中的一些优化操作,有一些是介绍如何获取 Django 查询转化的 sql 语句,有一些是理解 QuerySet 是如何获取数据的。
以下是本篇笔记目录:
- 性能方面
- 使用标准的数据库优化技术
- 理解 QuerySet
- 操作尽量在数据库中完成而不是在内存中
- 使用唯一索引来查询单个对象
- 如果知道需要什么数据,那么就立刻查出来
- 不要查询你不需要的数据
- 使用批量的方法
1、性能方面
1. connection.queries
前面我们介绍过 connection.queries 的用法,比如我们执行了一条查询之后,可以通过下面的方式查到我们刚刚的语句和耗时
>>> from django.db import connection
>>> connection.queries
[{'sql': 'SELECT polls_polls.id, polls_polls.question, polls_polls.pub_date FROM polls_polls',
'time': '0.002'}]
仅仅当系统的 DEBUG 参数设为 True,上述命令才可生效,而且是按照查询的顺序排列的一个数组
数组的每一个元素都是一个字典,包含两个 Key:sql 和 time
sql 为查询转化的查询语句
time 为查询过程中的耗时
因为这个记录是按照时间顺序排列的,所以 connection.queries[-1] 总能查询到最新的一条记录。
多数据库操作
如果系统用的是多个数据库,那么可以通过 connections['db_alias'].queries 来操作,比如我们使用的数据库的 alias 为 user:
>>> from django.db import connections
>>> connections['user'].queries
如果想清空之前的记录,可以调用 reset_queries() 函数:
from django.db import reset_queries
reset_queries()
2. explain
我们也可以使用 explain() 函数来查看一条 QuerySet 的执行计划,包括索引以及联表查询的的一些信息
这个操作就和 MySQL 的 explain 是一样的。
>>> print(Blog.objects.filter(title='My Blog').explain())
Seq Scan on blog (cost=0.00..35.50 rows=10 width=12)
Filter: (title = 'My Blog'::bpchar)
也可以加一些参数来查看更详细的信息:
>>> print(Blog.objects.filter(title='My Blog').explain(verbose=True, analyze=True))
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
之前在使用 Django 的过程中还使用到一个叫 silk 的工具,它可以用来分析一个接口各个步骤的耗时,有兴趣的可以了解一下。
2、使用标准的数据库优化技术
数据库优化技术指的是在查询操作中 SQL 底层本身的优化,不涉及 Django 的查询操作
比如使用 索引 index,可以使用 Meta.indexes 或者字段里的 Field.db_index 来添加索引
如果频繁的使用到 filter()、exclude()、order_by() 等操作,建议为其中查询的字段添加索引,因为索引能帮助加快查询
3、理解 QuerySet
1. 理解 QuerySet 获取数据的过程
1) QuerySet 的懒加载
一个查询的创建并不会访问数据库,直到获取这条查询语句的具体数据的时候,系统才会去访问数据库:
>>> q = Entry.objects.filter(headline__startswith="What") # 不访问数据库
>>> q = q.filter(pub_date__lte=datetime.date.today()) # 不访问数据库
>>> q = q.exclude(body_text__icontains="food") # 不访问数据库
>>> print(q) # 访问数据库
比如上面四条语句,只有最后一步,系统才会去查询数据库。
2) 数据什么时候被加载
迭代、使用步长分片、使用len()函数获取长度以及使用list()将QuerySet 转化成列表的时候数据才会被加载
这几点情况在我们的第九篇笔记中都有详细的描述。
3) 数据是怎么被保存在内存中的
每一个 QuerySet 都会有一个缓存来减少对数据库的访问操作,理解其中的运行原理能帮助我们写出最有效的代码。
当我们创建一个 QuerySet 的之后,并且数据第一次被加载,对数据库的查询操作就发生了。
然后 Django 会保存 QuerySet 查询的结果,并且在之后对这个 QuerySet 的操作中会重复使用,不会再去查询数据库。
当然,如果理解了这个原理之后,用得好就OK,否则会对数据库进行多次查询,造成性能的浪费,比如下面的操作:
>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])
上面的代码,同样一个查询操作,系统会查询两遍数据库,而且对于数据来说,两次的间隔期之间,Entry 表可能的某些数据库可能会增加或者被删除造成数据的不一致。
为了避免此类问题,我们可以这样复用这个 QuerySet :
>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # 查询数据库
>>> print([p.pub_date for p in queryset]) # 从缓存中直接使用,不会再次查询数据库
这样的操作系统就只执行了一遍查询操作。
使用数组的切片或者根据索引(即下标)不会缓存数据
QuerySet 也并不总是缓存所查询的结果,如果只是获取一个 QuerySet 部分数据,会查询有是否这个 QuerySet 的缓存
有的话,则直接从缓存中获取数据,没有的话,后续也不会将这部分数据缓存到系统中。
举个例子,比如下面的操作,在缓存整个 QuerySet 数据前,查询一个 QuerySet 的部分数据时,系统会重复查询数据库:
>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # 查询数据库
>>> print(queryset[5]) # 再次查询数据库
而在下面的操作中,整个 QuerySet 都被提前获取了,那么根据索引的下标获取数据,则能够从缓存中直接获取数据:
>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # 查询数据库
>>> print(queryset[5]) # 使用缓存
>>> print(queryset[5]) # 使用缓存
如果一个 QuerySet 已经缓存到内存中,那么下面的操作将不会再次查询数据库:
>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)
2. 理解 QuerySet 的缓存
除了 QuerySet 的缓存,单个 model 的 object 也有缓存的操作。
我们这里简单理解为外键和多对多的关系。
比如下面外键字段的获取,blog 是 Entry 的一个外键字段:
>>> entry = Entry.objects.get(id=1)
>>> entry.blog # Blog 的实例被查询数据库获得
>>> entry.blog # 第二次获取,使用缓存信息,不会查询数据库
而多对多关系的获取每次都会被重新去数据库获取数据:
>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all() # 查询数据库
>>> entry.authors.all() # 再次查询数据库
当然,以上的操作,我们都可以通过 select_related() 和 prefetch_related() 的方式来减少数据库的访问,这个的用法在前面的笔记中有介绍。
4、操作尽量在数据库中完成而不是在内存中
举几个例子:
- 在大多数查询中,使用 filter() 和 exclude() 在数据库中做过滤,而不是在获取所有数据之后在 Python 里的 for 循环里筛选数据
- 在同一个 model 的操作中,如果有涉及到其他字段的操作,可以用到 F 表达式
- 使用 annotate 函数在数据库中做聚合(aggregate)的操作
如果某些查询比较复杂,可以使用原生的 SQL 语句,这个操作也在前面有过一篇完整的笔记介绍过
5、使用唯一索引来查询单个对象
在使用 get() 来查询单条数据的时候,有两个理由使用唯一索引(unique)或 普通索引(db_index)
一个是基于数据库索引,查询会更快,
另一个是如果多条数据都满足查询条件,查询会慢得多,而在唯一索引的约束下则保证这种情况不会发生
所以使用下面的 id 进行匹配 会比 headline 字段匹配快得多,因为 id 字段在数据库中有索引且是唯一的:
entry = Entry.objects.get(id=10)
entry = Entry.objects.get(headline="News Item Title")
而下面的操作可能会更慢:
entry = Entry.objects.get(headline__startswith="News")
首先, headline 字段上没有索引,会导致数据库获取速度慢
其次,查询并不能保证只返回一个对象,如果匹配上来多个对象,且从数据库中检索并返回数百数千条记录,后果会很严重,其实就会报错,get() 能接受的返回只能是一个实例数据。
6、如果知道需要什么数据,那么就立刻查出来
能一次性查询所有需要的相关的数据的话,就一次性查询出来,不要在循环中做多次查询,因为那样会多次访问数据库
所以这就需要理解并且用到 select_related() 和 prefetch_related() 函数
7、不要查询你不需要的数据
1. 使用 values() 和 values_list() 函数
如果需求仅仅是需要某几个字段的数据,可以用到的数据结构为 dict 或者 list,可以直接使用这两个函数来获取数据
2. 使用 defer() 和 only()
如果明确知道只需要,或者不需要什么字段数据,可以使用这两个方法,一般常用在 textfield 上,避免加载大数据量的 text 字段
3. 使用 count()
如果想要获取总数,使用 count() 方法,而不是使用 len() 来操作,如果数据有一万条,len() 操作会导致这一万条数据都加载到内存里,然后计数。
4. 使用 exists()
如果仅仅是想查询数据是否至少存在一条可以使用 if QuerySet.exists() 而不是 if queryset 的形式
5. 使用 update() 和 delete()
能够批量更新和删除的操作就使用批量的方法,挨个去加载数据,更新数据,然后保存是不推荐的
6. 直接使用外键的值
如果需要外键的值,直接调用早就在这个 object 中的字段,而不是加载整个关联的 object 然后取其主键id
比如推荐:
entry.blog_id
而不是:
entry.blog.id
7. 如果不需要排序的结果,就不要order_by()
每一个字段的排序都是数据库的操作需要额外消耗性能的,所以如果不需要的话,尽量不要排序
如果在 Meta.ordering 中有一个默认的排序,而你不需要,可以通过 order_by() 不添加任何参数的方法来取消排序
为数据库添加索引,可以帮助提高排序的性能
8、使用批量的方法
1. 批量创建
对于多条 model 数据的创建,尽可能的使用 bulk_create() 方法,这是要优于挨个去 create() 的
2. 批量更新
bulk_update 方法也优于挨个数据在 for 循环中去 save()
3. 批量 insert
对于 ManyToMany 方法,使用 add() 方法的时候添加多个参数一次性操作比多次 add 要好
my_band.members.add(me, my_friend)
要优于:
my_band.members.add(me)
my_band.members.add(my_friend)
4. 批量 remove
当去除 ManyToMany 中的数据的时候,也是能一次性操作就一次性操作:
my_band.members.remove(me, my_friend)
要好于:
my_band.members.remove(me)
my_band.members.remove(my_friend)
如果想获取更多后端相关文章,可扫码关注阅读:
Django笔记二十八之数据库查询优化汇总的更多相关文章
- Java学习笔记二十八:Java中的接口
Java中的接口 一:Java的接口: 接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明.一个类通过继承接口的方式,从而来继承 ...
- Java基础学习笔记二十八 管家婆综合项目
本项目为JAVA基础综合项目,主要包括: 熟练View层.Service层.Dao层之间的方法相互调用操作.熟练dbutils操作数据库表完成增删改查. 项目功能分析 查询账务 多条件组合查询账务 添 ...
- angular学习笔记(二十八-附2)-$http,$resource中的promise对象
下面这种promise的用法,我从第一篇$http笔记到$resource笔记中,一直都有用到: HttpREST.factory('cardResource',function($resource) ...
- angular学习笔记(二十八-附1)-$resource中的资源的方法
通过$resource获取到的资源,或者是通过$resource实例化的资源,资源本身就拥有了一些方法,$save,$delete,$remove,可以直接调用来保存该资源: 比如有一个$resour ...
- angular学习笔记(二十八)-$http(6)-使用ngResource模块构建RESTful架构
ngResource模块是angular专门为RESTful架构而设计的一个模块,它提供了'$resource'模块,$resource模块是基于$http的一个封装.下面来看看它的详细用法 1.引入 ...
- Java笔记(二十八)……IO流下 IO包中其他常用类以及编码表问题
PrintWriter打印流 Writer的子类,既可以接收字符流,也可以接收字节流,还可以接收文件名或者文件对象,非常方便 同时,还可以设置自动刷新以及保持原有格式写入各种文本类型的print方法 ...
- PHP学习笔记二十八【抽象类】
<?php //定义一个抽象类.主要用来被继承 //如果一个类继承了抽象类,则它必须实现该抽象类的所有抽象方法(除非它自己也是抽象类) // abstract class Animal{ pub ...
- 论文阅读笔记二十八:You Only Look Once: Unified,Real-Time Object Detection(YOLO v1 CVPR2015)
论文源址:https://arxiv.org/abs/1506.02640 tensorflow代码:https://github.com/nilboy/tensorflow-yolo 摘要 该文提出 ...
- Python笔记(二十八)_魔法方法_迭代器
迭代器用于遍历容器中的数据,但它不是容器,它是一个实现了__next__方法的对象 与迭代器相关的内置函数: iter(): 将一个对象转换成一个迭代器 next(): 访问迭代器中的下一个变量,直到 ...
- django笔记二之数据库
django笔记二之数据库 [同步数据库之前的操作] yum install MySQL-python.x86_64 -y 2)开启数据库服务并创建表 创建数据库设置 为utf8: create da ...
随机推荐
- RKO组——冲刺随笔(2)
这个作业属于哪个课程 至诚软工实践F班 这个作业要求在哪里 第五次团队作业:项目冲刺 这个作业的目标 记录冲刺计划.要求包括当天会议照片.会议内容以及项目燃尽图(项目进度) 1.昨日进展 已开始着手模 ...
- 【原创】解决Multiple dex files define问题的思路
Multiple dex files define Landroid/support/v4/media/MediaMetadataCompat$Builder; 工作中我们可能会遇到各种 muxtip ...
- HDFS Property列表,适用于Hadoop 2.4以上 。
Property列表链接:http://hadoop.apache.org/docs/r2.4.1/hadoop-project-dist/hadoop-hdfs/hdfs-default.xml 以 ...
- Blob文件下载type类型
let url = window.URL.createObjectURL(new Blob([文件流(一般为res.data)], {type: "Blob类型"}) let li ...
- SAN证书(转载)
日常在周末更新相关容器,更新 potainer 2.6.3 后发现所有远程 docker 节点都无法连接了,看了下日志报错是这样的: background schedule error (endpoi ...
- (Winform程序带源码) 弹出输入框和获取输入框的值
弹出输入框和获取输入框的值: private void button1_Click(object sender, EventArgs e) { string returnValue = Microso ...
- 关于Appium执行用例过程中问题处理办法
关于Appium执行用例过程中问题处理办法 1. 运行环境 1.1 windows10 64位系统 1.2 华为荣耀V10 Android 9 1.3 appium-desktop 1 ...
- 在Linux中安装containerd作为kubernetes的容器运行时
概述 从kubernetes1.24开始的版本移除了内置的docker支持,用户可以自行选择需要使用的容器运行时,比如containerd.CRI-O.Docker Engine等等,这里我们采用二进 ...
- 有奖征文活动:从 RTC 到 RTE,从音视频到「实时万象」!
Hi 小伙伴们, 社区已经更名 RTE 两个月辣-大家对于 RTC 和 RTE 的区分,是否还有疑惑呢? 关于这两者的区别,我们创始人兼 CEO 赵斌老师说: "RTC(实时音视频)从 Co ...
- Java里的对象是咋回事
前言 在上一篇文章中,壹哥给大家介绍了Java中的类及其特点.创建过程等内容,相信你现在已经知道该如何创建一个Java类了.接下来在本篇文章中,壹哥会继续带大家学习面向对象中关于对象的内容.其实类和对 ...