模型层进阶相关
选择合适的层级工作
要在对应的level
(MVC) 做对应的事. 例如计算 count, 在最低的数据库 level 里是最快的 (如果只需要知道此记录是否存在的话,用 exists()
会更快).
但要 注意
: queryset 是 lazy 的,所以有时候在 higher level (例如模板) 里控制 queryset 是否真的执行,说不定会更高效.
下面这段代码很好的解释了不同 level 的意思:
# QuerySet operation on the database
# fast, because that's what databases are good at
# 执行效率最快, 属于数据库层级
my_bicycles.count()
# counting Python objects
# slower, because it requires a database query anyway, and processing
# of the Python objects
# 很慢, 属于在Python对象的级别层次处理
len(my_bicycles)
# Django template filter
# slower still, because it will have to count them in Python anyway,
# and because of template language overheads
# 仍然很慢, 模板层的本质还是需要在Python层面上进行数据的处理
{{ my_bicycles|length }}
理解QuerySet对象
切片
QuerySet可以支持切片语法, 这等同于SQL的limit和offset. 但是它不支持负数索引
print(models.Book.objects.all()[:4])
print(models.Book.objects.all()[4:8])
(0.002) SELECT `app01_book`.`id`, `app01_book`.`name`, `app01_book`.`pub_time`, `app01_book`.`publish_id` FROM `app01_book` LIMIT 4;
(0.000) SELECT `app01_book`.`id`, `app01_book`.`name`, `app01_book`.`pub_time`, `app01_book`.`publish_id` FROM `app01_book` LIMIT 4 OFFSET 4;
可迭代
QuerySet对象支持迭代.
for book in models.Book.objects.filter(pk__gt=5):
print(book)
惰性查询
惰性查询是Queryset对象的一个比较重要的特性. 看下面这个例子
q = models.Book.objects.filter(name__startswith='西')
q = q.filter(pk__gt=5)
q = q.filter(pub_time__year=2018)
print(q)
这上面的例子看起来是对数据库进行了3次查询, 但是实际上只有执行打印的时候才真正查询数据库了. 创建查询集后只有我们需要获取具体的数据, 然后orm才会去数据库"请求"值给我们.
官方推荐写法:
q = models.Book.objects.filter(
name__startswith='西'
).filter(
pk__gt=5
).filter(
pub_time__year=2018
)
那么什么才是后去具体数据的时机呢? 官方文档描述了下列几种情形.
- Iteration: ie. 对 Queryset 进行 For 循环的操作.
- slice:
q = models.Book.objects.all()[5:10:2]
当指定了步长的切片才会马上去执行数据库查询. - picling/caching
- repr/str
- len (Note: 如果你只想知道这个 queryset 结果的长度的话,最高效的还是在数据库的层级调用 count () 方法,也就是 sql 中的 COUNT ().)
- list()
- bool()
缓存机制
每个查询集都包含一个缓存来最小化对数据库的请求, 充分理解缓存的工作机制能帮助我们写出高效的代码.
当我们创建了一个新的查询集之后, 一旦发生了上面描述的7种情形, 就会在请求数据库之后, 可能生成cache(保存在查询集对象内),之后对相同的查询集做操作就不会重新去请求数据库获取数据了.
可以看看下面的结果
# 第一种方式
print([p.name for p in models.Publish.objects.all()])
print([p.addr for p in models.Publish.objects.all()])
# 第二种方式
q = models.Publish.objects.all()
print([p.name for p in q])
print([p.addr for p in q])
第一种方式实际是请求了两次数据库, QuerySet对象生成之后就直接弃用了, 缓存机制没有用上.
第二种方式只请求了一次数据库, 在第一次遍历QuerySet之后, 就将结果缓存起来了, 接下来就是对同一个QuerySet对象进行Python层面上的操作了.
会发生缓存的情形
[entry for entry in queryset] # 遍历整个查询集
bool(queryset) # 做布尔值运算
entry in queryset # in运算
list(queryset) # 转换成列表
特别要注意一下这些是不会发生缓存的.
q = models.Publish.objects.all()
print(q[2:]) # 做切片操作, 这里会查询数据库, 但不会将结果缓存到原来的查询集中
print(q[2]) # 做索引操作, 也会查询数据库, 也不会将结果缓存.
print(q) # 这里单纯的打印不会发生缓存
print(q)
# values, values_list都不会发生缓存. 下面也会发生
print(q.values('name', 'addr'))
print(q.values('addr'))
查询优化
官方提供的几种优化策略
- 利用
queryset lazy
的特性去优化代码,尽可能的减少连接数据库的次数. - 如果查出的 queryset 只用一次,可以使用 iterator () 去来防止占用太多的内存,
- 尽可能把一些数据库层级的工作放到数据库,例如使用 filter/exclude, F, annotate, aggregate (可以理解为 groupby)
- 一次性拿出所有你要的数据,不去取那些你不需要的数据.
意思就是要巧用 select_related (), prefetch_related () 和 values_list (), values (), 例如如果只需要 id 字段的话,用 values_list ('id', flat=True) 也能节约很多资源。或者使用defer()
和only()
方法:不加载某个字段 (用到这个方法就要反思表设计的问题了) / 只加载某些字段. - 如果不用 select_related 的话,去取外键的属性就会连数据再去查找.
- bulk (批量) 地去操作数据,比如
bulk_create
- 查找一条数据时,尽量用有索引的字段去查询,O (1) 或 O (log n) 和 O (n) 差别还是很大的
- 用
count()
代替len(queryset)
, 用exists()
代替if queryset:
下面再详细总结其中几种优化方式
select_related
对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用 select_related 来对QuerySet进行优化。
select_related 返回一个QuerySet
,当执行它的查询时它沿着外键关系查询关联的对象的数据。它会生成一个复杂的查询并引起性能的损耗,但是在以后使用外键关系时将不需要数据库查询。
简单说,在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。
下面是它和普通查询的区别
# 普通查询
book = models.Book.objects.filter(pk=2).first() # type: models.Book
print(book.publish.name)
SELECT
`app01_book`.`id`,
`app01_book`.`name`,
`app01_book`.`pub_time`,
`app01_book`.`publish_id`
FROM
`app01_book`
WHERE
`app01_book`.`id` = 2
ORDER BY
`app01_book`.`id` ASC
LIMIT 1;
SELECT
`app01_publish`.`id`,
`app01_publish`.`name`,
`app01_publish`.`addr`,
`app01_publish`.`pub_detail_id`
FROM
`app01_publish`
WHERE
`app01_publish`.`id` = 2;
上面的查询一共执行了两句sql语句.
使用select_related方法来执行查询的效率之比较.
books = models.Book.objects.filter(pk__lt=4).select_related('publish')
for book in books:
print(book.publish.name)
SELECT
`app01_book`.`id`,
`app01_book`.`name`,
`app01_book`.`pub_time`,
`app01_book`.`publish_id`,
`app01_publish`.`id`,
`app01_publish`.`name`,
`app01_publish`.`addr`,
`app01_publish`.`pub_detail_id`
FROM
`app01_book`
INNER JOIN `app01_publish` ON ( `app01_book`.`publish_id` = `app01_publish`.`id` )
WHERE
`app01_book`.`id` < 4;
由于使用了select_related提前将字段关联, 后面的跨表查询并没有继续操作数据库.
select_related还支持连接多个外键, 可以通过一个外键字段一直关联下去. 下面就是跨了3张表
books = models.Book.objects.filter(pk__lt=3).select_related('publish__pub_detail')
for book in books:
print(book.publish.pub_detail.email)
小结:
- select_related主要针一对一和多对一关系进行优化。
- select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
- 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。
- 没有指定的字段不会缓存,如果要访问的话Django会再次进行SQL查询。
对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。
prefetch_related
prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi 行的结果表。
prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。
# 只查询了两次数据库
books = models.Book.objects.prefetch_related('authors')
for book in books:
print(book.authors.all())
defer与only
only(*field)
: 返回一个对象, 只对括号内的字段属性做了查询优化
defer(*field)
: 返回一个对象, 对括号外的字段属性做了优化, 与only相反
上面依然可以获取优化之外的字段属性, 但是却需要进行数据库的查询获取.
books = models.Book.objects.only('name', 'pk')
books2 = models.Book.objects.values('name', 'pk')
books3 = models.Book.objects.defer('id')
print(books)
print(books2)
print(books3)
SELECT `app01_book`.`id`, `app01_book`.`name` FROM `app01_book` LIMIT 21;
SELECT `app01_book`.`name`, `app01_book`.`id` FROM `app01_book` LIMIT 21;
SELECT `app01_book`.`id`, `app01_book`.`name`, `app01_book`.`pub_time`, `app01_book`.`publish_id` FROM `app01_book` LIMIT 21;
从上面的执行sql语句可以上看出来, only和values执行的是一样的, 只是only返回的是列表套对象, 而values是列表套字典的形式. defer原理与only一样, 查询的是与only相反的数据. 所以如果只需要用到很少的数据, 又需要一个对象的形式, 就可以用到上面两个方法.
only, defer不能跨表优化, 就像下面这样, 有多少书, 就需要执行多少次数据库, 效率非常低下.
books = models.Book.objects.only('pk', 'publish')
for book in books:
print(book.pk, book.publish.name)
事务优化
事务操作不仅能够保证数据的安全, 还有一个很有用过的作用就是, 可以通过事务隔离Django默认的autocommit, 来避免Django频繁的向数据库提交数据. 这也能够很好的提升性能.
在Django中开启事务的语法非常简单.
from django.db import transaction
with transaction.atomic():
pass
批量操作
在QuerySet中有许多批量操作的方式, 例如delete
update
bulk_create
...
这些批量操作对应于数据库层面的批量操作, 能够有效的防止批频繁请求数据库.
details = [models.PublishDetail(email=f'email{i}') for i in range(5)]
for d in details:
d.save()
# 批量操作
models.PublishDetail.objects.bulk_create(details)
上面for循环5次, 需要请求数据库5次, 而bulk_create只需要请求数据库一次. 数据越多, 效率上的差距越明显.