浅谈优化Django ORM中的性能问题


Posted in Python onJuly 09, 2020

Django是个好工具,使用的很广泛。 在应用比较小的时候,会觉得它很快,但是随着应用复杂和壮大,就显得没那么高效了。当你了解所用的Web框架一些内部机制之后,才能写成比较高效的代码。

怎么查问题

Web系统是个挺复杂的玩意,有时候有点无从下手哈。可以采用 自底向上 的顺序,从数据存储一直到数据展现,按照这个顺序一点一点查找性能问题。

数据库 (缺少索引/数据模型)

数据存储接口 (ORM/低效的查询)

展现/数据使用 (Views/报表等)

Web应用的大部分问题都会跟 数据库 扯上关系。除非你正在处理大量的数据并知道你在做什么,否则不要去考虑用Big-O表示法思考View的问题。 数据库调用的开销将使循环和模板渲染的开销相形见绌。 不首先解决数据库使用中的问题,您就不能继续解决其他问题。

Django的文档中有那么一节,详细的描述了DB部分优化, ORM 从一开始就应该写的比较高效一些(毕竟有那么多最佳实践)

优化,很多时候意味着代码可能变得不太清晰。当你遇到选择清晰的代码,还是牺牲清晰代码来获取性能上的一点点提高的时候,请优先考虑要代码的清晰整洁

工具

解决问题的第一步是找到问题,面对 ORM,有时间事情可以做。

理解 django.db.connection, 这个对象可以用来记录当前查询花费的时间(知道了SQL语句查询的时间,当然就知道那里慢了)

>>> from django.db import connection
>>> connection.queries
[]
>>> Author.objects.all()
<QuerySet [<Author: Author object>]>
>>> connection.queries
[{u'time': u'0.002', u'sql': u'SELECT "library_author"."id", "library_author"."name" FROM "library_author" LIMIT 21'}]

但是使用起来好像不是很方面。

在shell命令行的环境下,可以使用 django-exension's shell_plus 命令并打开 --print-sql 选项。

python manage.py shell_plus --print-sql

>>> Author.objects.all()
SELECT "library_author"."id", "library_author"."name" FROM "library_author" LIMIT 21
Execution time: 0.001393s [Database: default]
<QuerySet [<Author: Author object>]>

还有个更方面的方式, 使用 Django-debug-toolbar 工具,就可以在web端查看SQL查询的详细统计结果,其实它功能远不止这个。

总结下3个方式

django.db.connection django自身提供,比较底层

django-extensions 可以在shell环境下方面调试

django-debug-toolbar 可以在web端直接看到debug结果

案例

下面是用个具体的例子来说明一些问题

model 定义

很经典的外键关系, Author 和 Book 一对多的关系

class Author(models.Model):
 name = models.TextField()

class Book(models.Model):
 title = models.TextField()
 author = models.ForeignKey(
 Author, on_delete=models.PROTECT, related_name='books', null=True
 )

多余的查询

当你检查一个book是否有author或者想获取这本书的author 的id的时候,可能更倾向于直接使用 author 对象。

if book.author:
 do_stuff()
# Or
do_stuff_with_author_id(book.author.id)

这里 author对象 其实并不需要(主要指第一行代码,其实只需要author_id),会导致一次多余的查询。 如果后面需要 author对象,在获取也不冲突。 比较好的习惯是,直接使用字段名, 见下面的写法。

if book.author_id:
 do_stuff()

do_stuff_with_author_id(book.author_id)

count 和 exists

对于初学者, 知道什么时候使用 count 和 exists 还是挺难的。 Django会缓存查询结果, 所以如果后续的操作会用到这些查询出来的数据 ,可以使用 Python的内置方法(指的是len,if判断queryset,下面例子)。如果不用查询出的数据,使用queryset提供的方法(count(), exists())

# Don't waste a query if you are using the queryset
books = Book.objects.filter(..)
if books:
 do_stuff_with_books(books)

# If you aren't using the queryset use exist
books = Book.objects.filter(..)
if books.exists():
 do_some_stuff()

# But never
if Book.objects.filter(..):
 do_some_stuff()

下面是关于count 和 len 的例子

# Don't waste a query if you are using the queryset
books = Book.objects.filter(..)
if len(books) > 5:
 do_stuff_with_books(books)

# If you aren't using the queryset use count
books = Book.objects.filter(..)
if books.count() > 5:
 do_some_stuff()

# But never
if len(Book.objects.filter(..)) > 5:
 do_some_stuff()

只获取需要的数据

默认情况下,ORM 查询的时候会把数据库记录对应的所有列取出来,然后转换成 Python对象,这无疑是个很大的浪费嘛(有时候只想要一两个列的,宝宝心理��)。当你只需要某些列的时候可以使用 values 或者 values_list, 它们不是把数据转换成复杂的 python 对象,而是dicts, tuples等。

# Retrieve values as a dictionary
>>> Book.objects.values('title', 'author__name')
<QuerySet [{'author__name': u'Nikolai Gogol', 'title': u'The Overcoat'}, {'author__name': u'Leo Tolstoy', 'title': u'War and Peace'}]>

# Retrieve values as a tuple
>>> Book.objects.values_list('title', 'author__name')
<QuerySet [(u'The Overcoat', u'Nikolai Gogol'),
(u'War and Peace', u'Leo Tolstoy')]>
>>> Book.objects.values_list('title')
<QuerySet [(u'The Overcoat',), (u'War and Peace',)]>

# With one value, it is easier to flatten the list
>>> Book.objects.values_list('title', flat=True)
<QuerySet [u'The Overcoat', u'War and Peace']>

处理很多记录

当你获得一个 queryset 的时候,Django会缓存这些数据。 如果你需要对查询结果进行好几次循环,这种缓存是有意义的,但是对于 queryset 只循环一次的情况,缓存就没什么意义了。

for book in Books.objects.all():

do_stuff(book)

上面的查询,django会把books所有的数据欧载入内存,然后进行一次循环。其实我们更想要保持这个数据库 connection, 每次循环的取出一条book数据,然后调用 do_stuff。iterator 就是我们的救星。

for book in Books.objects.all().iterator():

do_stuff(book)

有了 iterator,你就可以编写线性数据表或者CSV流了。就能增量写入文件或者发送给用户。

特别是跟 values,values_list 结合在一起的时候,能尽可能少的使用内存。在需要对表中的每一行进行修改的迁移期间,使用iterator也非常方便。 不能因为迁移不是面向客户的就可以降低对效率的要求。 长时间运行的迁移可能意味着事务锁定或停机。

关联查询问题

Django ORM的API使得我们使用关系型数据库的时候就像使用面向对象的 Python 语言那样自然。

# Get the Author's name of a Book
book = Book.objects.first()
book.author.name

上面的代码相当的清晰和好理解。Django 使用 lazy loading(懒加载)的方式,只有用到了 author 对象时候才会加载。这样做有好处,但是会造成爆炸��式的查询。

>>> Author.objects.count()
20
>>> Book.objects.count()
100
# This block is 101 queries.
# 1 for the books and 1 for each author that lazy-loaded 
books = Book.objects.all()
for book in books:
 do_stuff(book.title, book.author.name)

# This block is 20 queries.
# 1 for the author and 1 for the books of each author
authors = Author.objects.all()
for author in authors:
 do_stuff_with_books(author.name, author.books.all())

Django 意识到了这种问题,并提供 select_related 和 prefetch_related 来解决。

# This block is 1 query
# The authors of all the books are pre-fetched in one query
book = Book.objects.selected_related('author').all()
for book in books:
 do_stuff(book.title, book.author)

# This block is 1 query
# The books of all the authors are pre-fetched in one query
authors = Author.objects.prefetch_related('books').all()
for author in authors:
 do_stuff_with_books(author.name, author.books.all())

在Django app中使用 prefetch_related 和 select_related 的时候要谨慎。

prefetch_related 有个坑,当你像要在related查询中使用 filter时候author.books.filter(..), 之前在 prefetch_related 中的缓存就无法使用了,相对于 author.books.all() 来说的。有些事情会变的复杂了,你最好2次查询来解决这种问题,上级对象和它的子对象各一次,然后在进行聚合。 如果 prefetch太复杂了,这时候就要在代码的整洁清晰和应用性能之间做一个取舍了。

最好是了解下 prefetch_related 和 select_related 的区别,文档在这

select_related 不好用的时候

某些情况下 select_related 会变得不好使。 看看下面的例子,id() 方法用来判断 Python 对象实例的唯一性,如果 id结果相同,表示同一个 对象实例。

>>> [(id(book.author), book.author.pk) for book in Book.objects.select_related('author')]

[(4504798608, 1), (4504799824, 1)]

select_related 为查询的每个row,创建了一个新对象,耗费了大量的内存(上面的结果中,对于数据库中的同一个author对象创建了不同的python对象)。SQL一会为每行返回重复的信息。 如果你进行一个查询,其中select_related 查询的所有值都是相同的,你就需要使用别的东西。 使用相关查询或翻转(flip)查询并使用prefetch_related。

使用 author.books.all() 结合对象相关查询,Django会为每个已经查询的book记录保存相同的author对象

>> id(author)
4504693520
>>> [(id(book.author), book.author.pk) for book in author.books.all()]
[(4504693520, 1), (4504693520, 1)]

使用 select_related 还有一个隐含问题,当你修改一个author 对象的时候,如果其他book也关联到这个author,这个改变不会传播过去,因为它们在python内存中是不同的对象实例。如果使用 对象相关查询,修改就能传播。

简单不一定更好

Django使得关系查询太容易了,这也带来了一些副作用。当你将一个对象传入函数中,接着使用了 relationship (对象关系), 实际上无法知道这种关联的数据是否已经从数据库取出来。

def author_name_length(book):
 return len(book.author.name)

def process_author_books(author):
 for book in author.books.all():
 do_stuff(book)

上面的函数中 author_name_length 和 process_author_books, 谁将会查询? 我们无从所知。 Django ORM中的关联查询非常好用,我们自然希望使用这种方式。在一个循环中,如果不使用 select_related 或者 prefetch_related,可能会导致几百个查询。Django只会知道查询,而不会多看一眼。这种情况只能依靠SQL的logs,还有函数调用来监控,然后确定是否进行预查询。

我们可以重写函数,参数的传递采用扁平的数据结构,类似 namedtuple, 而不是 model,但这种别考虑这种方案。

怎么修复?

我们已经知道了这个问题,那么怎样拓展Django能让我们更明确的知道资源的消耗呢。很多数据库的封装已经通过不同的方式解决了这个问题。在Ecto中,Elixir的数据库封装,一个没有获取数据的关系调用会返回 Ecto.Association.NotLoaded 提示,而不是默默的查询。

我们可以想象Django的某个版本使用 pythonic 的方式实现了这种功能。

>>> book.author.name
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/kyle/orm_test/library/models.py", line 18, in __get__
'Use `select_related` or `fetch_{rel}`'.format(rel=self.field.name)
RelationNotLoaded: Relation `author` not loaded. Use `select_related` or `fetch_author`

# We explicitly fetch the resource
>>> book.fetch_author()
<Author: Author object>
>>> book.author.name
"Fyodor Dostoevsky"

# Select related works just as well
>>> book = Book.objects.select_related('author').first()
>>> book.author.name
"Anton Chekhov"

总结

ORM 的使用并没有固定的标准。对于小的应用来说,优化可能并没有多么明显的效果。应该以代码清晰为优先,然后在考虑优化的事情。程序增长过程中,对 ORM 的使用一定要保持好的习惯。养成对资源消耗敏感的习惯,以后会有很多好处。

优化的方法很多,对于长远来说了解一些原则更为实用

习惯隔离代码并记录产生的查询

不要在循环中查询

了解 ORM 是怎么缓存数据的

知道 Django 何时会做查询

不要以牺牲清晰度为代价过度优化

以上这篇浅谈优化Django ORM中的性能问题就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
在python的类中动态添加属性与生成对象
Sep 17 Python
浅谈pytorch和Numpy的区别以及相互转换方法
Jul 26 Python
python 对key为时间的dict排序方法
Oct 17 Python
浅谈Python中的bs4基础
Oct 21 Python
Python实现将字符串的首字母变为大写,其余都变为小写的方法
Jun 11 Python
Python爬虫实现的根据分类爬取豆瓣电影信息功能示例
Sep 15 Python
Python第三方包之DingDingBot钉钉机器人
Apr 09 Python
opencv之颜色过滤只留下图片中的红色区域操作
Jun 05 Python
python3.5的包存放的具体路径
Aug 16 Python
python能做哪些生活有趣的事情
Sep 09 Python
python 对一幅灰度图像进行直方图均衡化
Oct 27 Python
python3 os进行嵌套操作的实例讲解
Nov 19 Python
Python单元测试及unittest框架用法实例解析
Jul 09 #Python
python 抓取知乎指定回答下视频的方法
Jul 09 #Python
Django ORM判断查询结果是否为空,判断django中的orm为空实例
Jul 09 #Python
Python Selenium模块安装使用教程详解
Jul 09 #Python
OpenCV 使用imread()函数读取图片的六种正确姿势
Jul 09 #Python
django模型类中,null=True,blank=True用法说明
Jul 09 #Python
Python pip安装第三方库实现过程解析
Jul 09 #Python
You might like
Smarty+QUICKFORM小小演示
2007/02/25 PHP
php实现阳历阴历互转的方法
2015/10/28 PHP
浅析PHP类的反射来实现依赖注入过程
2018/02/06 PHP
在thinkphp5.0路径中实现去除index.php的方式
2019/10/16 PHP
JavaScript多线程的实现方法
2007/05/08 Javascript
javascript 哈希表(hashtable)的简单实现
2010/01/20 Javascript
jquery的$(document).ready()和onload的加载顺序
2010/05/26 Javascript
jquery中ajax学习笔记3
2011/10/16 Javascript
firefox下jQuery UI Autocomplete 1.8.*中文输入修正方法
2012/09/19 Javascript
流量统计器如何鉴别C#:WebBrowser中伪造referer
2015/01/07 Javascript
详解JavaScript的Date对象(制作简易钟表)
2020/04/07 Javascript
AngularJS 指令的交互详解及实例代码
2016/09/14 Javascript
使用UrlConnection实现后台模拟http请求的简单实例
2017/01/04 Javascript
JS实现动态修改table及合并单元格的方法示例
2017/02/20 Javascript
微信小程序 获取javascript 里的数据
2017/08/17 Javascript
js比较两个单独的数组或对象是否相等的实例代码
2019/04/28 Javascript
vue+iview框架实现左侧动态菜单功能的示例代码
2020/07/23 Javascript
python定时检查启动某个exe程序适合检测exe是否挂了
2013/01/21 Python
解决python写的windows服务不能启动的问题
2014/04/15 Python
利用Python操作消息队列RabbitMQ的方法教程
2017/07/19 Python
Python3多线程爬虫实例讲解代码
2018/01/05 Python
使用python将图片格式转换为ico格式的示例
2018/10/22 Python
Python OpenCV对本地视频文件进行分帧保存的实例
2019/01/08 Python
Python pip替换为阿里源的方法步骤
2019/07/02 Python
numpy按列连接两个维数不同的数组方式
2019/12/06 Python
Python设计密码强度校验程序
2020/07/30 Python
纯CSS3实现带动画效果导航菜单无需js
2013/09/27 HTML / CSS
css3+jq创作含苞待放的荷花
2014/02/20 HTML / CSS
html5 localStorage本地存储_动力节点Java学院整理
2017/07/06 HTML / CSS
如何现实servlet的单线程模式
2014/08/05 面试题
动物学专业毕业生求职信
2013/10/11 职场文书
2014机关党员干部“正风肃纪”思想汇报
2014/09/15 职场文书
2014年法务工作总结
2014/12/11 职场文书
骨干教师个人总结
2015/02/11 职场文书
创业计划书之家政服务
2019/09/18 职场文书
读《茶花女》有感:山茶花的盛开与凋零
2020/01/17 职场文书