优化Django ORM中的性能问题
来源:互联网 发布:不出门便知天下事言语 编辑:程序博客网 时间:2024/06/02 04:39
原文地址 Solving Performance Problems in the Django ORM
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 21Execution 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()# Ordo_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 querysetbooks = Book.objects.filter(..)if books: do_stuff_with_books(books)# If you aren't using the queryset use existbooks = Book.objects.filter(..)if books.exists(): do_some_stuff()# But neverif Book.objects.filter(..): do_some_stuff()
下面是关于count
和 len
的例子
# Don't waste a query if you are using the querysetbooks = Book.objects.filter(..)if len(books) > 5: do_stuff_with_books(books)# If you aren't using the queryset use countbooks = Book.objects.filter(..)if books.count() > 5: do_some_stuff()# But neverif 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 Bookbook = 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 authorauthors = 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 querybook = 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 queryauthors = 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.nameTraceback (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中的性能问题
- 优化Django ORM中的性能问题(含prefetch_related 和 select_related)
- Android中的性能优化问题
- Django性能优化
- Django性能优化
- Django性能优化
- Django性能优化
- Django性能数据优化
- [Django]orm中的外键级联删除
- Django中的ORM(对象关系映射)
- django 1.10 ORM单独使用的问题
- django orm
- django ORM
- Django ORM
- ASP.NET设计中的性能优化问题
- ASP.NET设计中的性能优化问题
- ASP.NET设计中的性能优化问题
- ASP.NET设计中的性能优化问题
- 迷茫
- ThinkingInJava——成员初始化
- mysql导入导出sql文件
- 公牛与状压dp
- Java学习笔记排序算法-------冒泡排序
- 优化Django ORM中的性能问题
- 关于C++合成的默认构造函数
- ReactNative嵌入到安卓原生中遇到的坑
- 安卓开发学习心得-------关于软键盘的那些知识与技巧
- 在RHEL7的vnc中调出Oracle的OUI
- BZOJ 4028: [HEOI2015]公约数数列 分块
- java安全架构____java 秘钥对原理
- 浅谈Activiti Modeler 的扩展
- Sorting It All Out POJ-1094