浅谈优化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 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中的性能问题就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 基于Django ORM、一对一、一对多、多对多的全面讲解

    上篇博客也提到这些知识点,可能大家还是不太清楚,这篇博客为大家详细讲解ORM中的几个知识点 1.1首先我们先看一个小案例: #_*_coding:utf-8_*_ from django.db import models # Create your models here. class Colors(models.Model): colors=models.CharField(max_length=10) #蓝色 def __str__(self): return self.colors cla

  • Django ORM判断查询结果是否为空,判断django中的orm为空实例

    我就废话不多说了,大家还是直接看代码吧~ result= Booking.objects.filter() #方法一 .exists() if result.exists(): print "QuerySet has Data" else: print "QuerySet is empty" #方法二 .count()==0 if result.count() == 0: print "empty" #方法三 if result: print &

  • Django的性能优化实现解析

    一 利用标准数据库优化技术 传统数据库优化技术博大精深,不同的数据库有不同的优化技巧,但重心还是有规则的.在这里算是题外话,挑两点通用的说说: 索引,给关键的字段添加索引,性能能更上一层楼,如给表的关联字段,搜索频率高的字段加上索引等.Django建立实体的时候,支持给字段添加索引,具体参考Django.db.models.Field.db_index.按照经验,Django建立实体之前应该早想好表的结构,尽量想到后面的扩展性,避免后面的表的结构变得面目全非. 使用适当字段类型,本来varcha

  • 浅谈Django REST Framework限速

    官方文档 settings.py配置 REST_FRAMEWORK = { 'DEFAULT_THROTTLE_CLASSES': ( 'rest_framework.throttling.AnonRateThrottle', 'rest_framework.throttling.UserRateThrottle' ), 'DEFAULT_THROTTLE_RATES': { 'anon': '100/day', 'user': '1000/day' } } AnonRateThrottle:用

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

    Django是个好工具,使用的很广泛. 在应用比较小的时候,会觉得它很快,但是随着应用复杂和壮大,就显得没那么高效了.当你了解所用的Web框架一些内部机制之后,才能写成比较高效的代码. 怎么查问题 Web系统是个挺复杂的玩意,有时候有点无从下手哈.可以采用 自底向上 的顺序,从数据存储一直到数据展现,按照这个顺序一点一点查找性能问题. 数据库 (缺少索引/数据模型) 数据存储接口 (ORM/低效的查询) 展现/数据使用 (Views/报表等) Web应用的大部分问题都会跟 数据库 扯上关系.除非

  • 浅谈Redis高并发缓存架构性能优化实战

    目录 场景1: 中小型公司Redis缓存架构以及线上问题实战 场景2: 大厂线上大规模商品缓存数据冷热分离实战 场景3: 基于DCL机制解决热点缓存并发重建问题实战 场景4: 突发性热点缓存重建导致系统压力暴增 场景5: 解决大规模缓存击穿导致线上数据库压力暴增 场景6: 黑客工资导致缓存穿透线上数据库宕机 场景7: 大V直播带货导致线上商品系统崩溃原因分析 场景8: Redis分布式锁解决缓存与数据库双写不一致问题实战 场景9: 大促压力暴增导致分布式锁串行争用问题优化 场景10: 利用多级缓

  • 浅谈在django中使用redirect重定向数据传输的问题

    环境: python 3.6.4 django2.0.6 使用重定向redirect('url name') 如果不需要传数据的话那这样就OK了 如果要传数据的话 我琢磨了半天 还是决定用session来传输 所以 就这么干: request.session['key_name] = value request.session['msg'] = u'用户未登录' 然后在模板中使用: <h1>{{ request.session.username }}</h1> {# 输出usern

  • 浅谈在django中使用filter()(即对QuerySet操作)时踩的坑

    代码伺候: 先看如下代码: 例1: message = Message.objects.filter(pk=message_id2) message[0].id = message_id2 message[0].content = content2 message[0].message_type = message_type2 print(message[0].id) print(message[0].content) message[0].save() 可正常从QuerySet中读取数据,并打

  • 浅谈Node.js ORM框架Sequlize之表间关系

    Sequelize模型之间存在关联关系,这些关系代表了数据库中对应表之间的主/外键关系.基于模型关系可以实现关联表之间的连接查询.更新.删除等操作.本文将通过一个示例,介绍模型的定义,创建模型关联关系,模型与关联关系同步数据库,及关系模型的增.删.改.查操作. 数据库中的表之间存在一定的关联关系,表之间的关系基于主/外键进行关联.创建约束等.关系表中的数据分为1对1(1:1).1对多(1:M).多对多(N:M)三种关联关系. 在Sequelize中建立关联关系,通过调用模型(源模型)的belon

  • 浅谈C/C++ 语言中的表达式求值

    经常可以在一些讨论组里看到下面的提问:"谁知道下面C语句给n赋什么值?" m = 1; n = m+++m++; 最近有位不相识的朋友发email给我,问为什么在某个C++系统里,下面表达式打印出两个4,而不是4和5: a = 4; cout << a++ << a; C++ 不是规定 << 操作左结合吗?是C++ 书上写错了,还是这个系统的实现有问题? 注:运行a = 4; cout << a++ << a; 如在Visua

  • 浅谈vue3在项目中的逻辑抽离和字段显示

    目录 逻辑分层 将各个区域业务分开 这样做的优势 这样的场景应该如何处理 优化 reactive 不一定非要写在setup函数中 如何在页面上直接显示值 逻辑分层 我们在使用vue3开发项目的时候, 如何进行[区域分层]呢???? 举一个简单的小粒子 一个区域有[查询逻辑.修改后的保存逻辑.新增逻辑.删除逻辑] 这个页面可能还有其他的区域.A区域.B区域,C区域...[有很多逻辑] 这个时候我们可以将一个区域的逻辑分离出去 将各个区域业务分开 export default { setup ()

  • 浅谈Spark RDD API中的Map和Reduce

    RDD是什么? RDD是Spark中的抽象数据结构类型,任何数据在Spark中都被表示为RDD.从编程的角度来看,RDD可以简单看成是一个数组.和普通数组的区别是,RDD中的数据是分区存储的,这样不同分区的数据就可以分布在不同的机器上,同时可以被并行处理.因此,Spark应用程序所做的无非是把需要处理的数据转换为RDD,然后对RDD进行一系列的变换和操作从而得到结果.本文为第一部分,将介绍Spark RDD中与Map和Reduce相关的API中. 如何创建RDD? RDD可以从普通数组创建出来,

  • 浅谈在JAVA项目中LOG4J的使用

    一.直接使用: //输出到项目文件夹下output1.txt文件中 ////////////////////////////// // DEBUG - Here is some DEBUG // INFO - Here is some INFO // WARN - Here is some WARN // ERROR - Here is some ERROR // FATAL - Here is some FATAL ////////////////////////////// package

  • 浅谈JS之iframe中的窗口

    1.window.self 对当前窗口自身的引用;self,window.self,window三者是等价的 2.window.top 对顶层窗口的引用,如果本身就是顶层窗口,则返回本身 3.window.parent 对父窗口的引用,如果没有父窗口,则返回本身 以上这篇浅谈JS之iframe中的窗口就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

随机推荐