这一篇笔记将从以下几个方面来介绍 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 笔记。
原文链接:Django 笔记二十八之数据库查问优化汇总
如果想获取更多相干文章,可扫码关注浏览: