乐趣区

关于python:Django笔记二十八之数据库查询优化汇总

这一篇笔记将从以下几个方面来介绍 Django 在查问过程中的一些优化操作,有一些是介绍如何获取 Django 查问转化的 sql 语句,有一些是了解 QuerySet 是如何获取数据的。

以下是本篇笔记目录:

  1. 性能方面
  2. 应用规范的数据库优化技术
  3. 了解 QuerySet
  4. 操作尽量在数据库中实现而不是在内存中
  5. 应用惟一索引来查问单个对象
  6. 如果晓得须要什么数据,那么就立即查出来
  7. 不要查问你不须要的数据
  8. 应用批量的办法

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、操作尽量在数据库中实现而不是在内存中

举几个例子:

  1. 在大多数查问中,应用 filter() 和 exclude() 在数据库中做过滤,而不是在获取所有数据之后在 Python 里的 for 循环里筛选数据
  2. 在同一个 model 的操作中,如果有波及到其余字段的操作,能够用到 F 表达式
  3. 应用 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 笔记二十八之数据库查问优化汇总

如果想获取更多相干文章,可扫码关注浏览:

退出移动版