发布网友 发布时间:2024-10-07 08:06
共1个回答
热心网友 时间:2024-10-07 08:39
导读:本篇文章首席CTO笔记来给大家介绍有关怎么解决django惰性查询的相关内容,希望对大家有所帮助,一起来看看吧。
如何优化DjangoRESTFramework的性能解决Django「懒惰」的基本方法
现在我们解决这个问题的方法就是「预加载」。从本质上讲,就是你提前警告DjangoORM你要一遍又一遍的告诉它同样无聊的指令。在上面的例子中,在DRF开始获取前很简单地加上这句话就搞定了:
queryset=queryset.prefetch_related('orders')
当DRF调用上述相同序列化customers时,出现的是这种情况:
获取所有customers(执行两个往返数据库操作,第一个是获取customers,第二个获取相关customers的所有相关的orders。)
对于第一个返回的customers,获取其order(不需要访问数据库,我们已经在上一步中获取了所需要的数据)
对于第二个返回的customers,获取其order(不需要访问数据库)
对于第三个返回的customers,获取其order(不需要访问数据库)
对于第四个返回的customers,获取其order(不需要访问数据库)
对于第五个返回的customers,获取其order(不需要访问数据库)
对于第六个返回的customers,获取其order(不需要访问数据库)
你又意识到,你可以有了很多customers,已经不需要再继续等待去数据库。
其实DjangoORM的「预备」是在第1步进行请求,它在本地高速缓存的数据能够提供步骤2+所要求的数据。与之前往返数据库相比从本地缓存数据中读取数据基本上是瞬时的,所以我们在有很多customers时就获得了巨大的性能加速。
解决DjangoRESTFramework性能问题的标准化模式
我们已经确定了一个优化DjangoRESTFramework性能问题的通用模式,那就是每当序列化查询嵌套字段时,我们就添加一个新的@staticmethod名叫setup_eager_loading,像这样:
classCustomerSerializer(serializers.ModelSerializer):
orders=OrderSerializer(many=True,read_only=True)
defsetup_eager_loading(cls,queryset):
"""Performnecessaryeagerloadingofdata."""
queryset=queryset.prefetch_related('orders')
returnqueryset
这样,不管哪里要用到这个序列化,都只需在调用序列化前简单调用setup_eager_loading,就像这样:
customer_qs=Customers.objects.all()
customer_qs=CustomerSerializer.setup_eager_loading(customer_qs)#SetupeagerloadingtoavoidN+1selects
post_data=CustomerSerializer(customer_qs,many=True).data
或者,如果你有一个APIView或ViewSet,你可以在get_queryset方法里调用setup_eager_loading:
defget_queryset(self):
queryset=Customers.objects.all()
#SetupeagerloadingtoavoidN+1selects
queryset=self.get_serializer_class().setup_eager_loading(queryset)
returnqueryset
如何有效的遍历django的QuerySet
开始的阶段没有遇到什么问题,我们举例,在models有一张员工表employee,对应的表结构中,postion列表示员工职位,前台post过来的参数赋给position,加上入职时间、离职时间,查询操作通过models.filter(position=params)完成,获取的员工信息内容由QuerySet和当前展示页与每页展示的记录数进行简单的计算,返回给前台页面进行渲染展示。编码如下:
defget_employees(position,start,end):
returnemployee.objects.filter(alert_time__lt=end,alert_time__gt=start).filter(position__in=position)
@login_required
defshow(request):
ifnotvalidate(request):
returnrender_to_response('none.html',
context_instance=RequestContext(request,'msg':'paramserror')
)
position=request.REQUEST.get('position')
time_range=request.REQUEST.get('time')
start,end=time_range[0],time_range[1]
num_per_page,page_num=get_num(request)
all_employees=get_employees(position,start,end)
#根据当前页与每页展示的记录数,取到正确的记录
employees=employees_events[(page_num-1)*num_per_page:page_num*num_per_page]
returnrender_to_response('show_employees.html',
context_instance=RequestContext(
request,
'employees':employees,
'num_per_page':num_per_page,
'page_num':page_num,
'page_options':[50,100,200]
)
)
运行之后可以正确的对所查询的员工信息进行展示,并且查询速度很快。employee表中存放着不同职位的员工信息,不同类型的详细内容也不相同,假设employees有一列名为infomation,存储的是员工的详细信息,infomation={'age':33,'gender':'male','nationality':'German','degree':'doctor','motto':'justdoit'},现在的需求是要展示出分类更细的员工信息,前台页面除了post职位、入职离职时间外,还会对infomation中的内容进行筛选,这里以查询中国籍的设计师为例,在之前的代码基础上,需要做一些修改。员工信息表employee存放于MySQL中,而MySQL为ORM数据库,它并未提供类似mongodb一样更为强大的聚合函数,所以这里不能通过objects提供的方法进行filter,一次性将所需的数据获取出来,那么需要对type进行过滤后的数据,进行二次遍历,通过information来确定当前记录是否需要返回展示,在展示过程中,需要根据num_per_page和page_num计算出需要展示数据起始以及终止位置。
defget_employees(position,start,end):
returnemployee.objects.filter(alert_time__lt=end,alert_time__gt=start).filter(position__in=position)
deffilter_with_nation(all_employees,nationality,num_per_page,page_num):
result=[]
pos=(page_num-1)*num_per_page
cnt=0
start=False
foremployeeinall_employees:
info=json.loads(employee.information)
ifinfo.nationality!=nationality:
continue
#获取的数据可能并不是首页,所以需要先跳过前n-1页
ifcnt==pos:
ifstart:
break
cnt=0
pos=num_per_page
start=True
ifstart:
result.append(employee)
returnemployee
@login_required
defshow(request):
ifnotvalidate(request):
returnrender_to_response('none.html',
context_instance=RequestContext(request,'msg':'paramserror')
)
position=request.REQUEST.get('position')
time_range=request.REQUEST.get('time')
start,end=time_range[0],time_range[1]
num_per_page,page_num=get_num(request)
all_employees=get_employees(position,start,end)
nationality=request.REQUEST.get('nationality')
employees=filter_with_nation(all_employees,num_per_page,page_num)
returnrender_to_response('show_employees.html',
context_instance=RequestContext(
request,
'employees':employees,
'num_per_page':num_per_page,
'page_num':page_num,
'page_options':[50,100,200]
)
)
当编码完成之后,在数据employee表数据很小的情况下测试并未发现问题,而当数据量非常大,并且查询的数据很少时,代码运行非常耗时。我们设想,这是一家规模很大的*公司,同时人员的流动量也很大,所以employee表的数据量很庞大,而这里一些来自于小国家的员工并不多,比如需要查询国籍为梵蒂冈的员工时,前台页面进入了无尽的等待状态。同时,监控进程的内存信息,发现进程的内存一直在增长。毫无疑问,问题出现在filter_with_nation这个函数中,这里逐条遍历了employee中的数据,并且对每条数据进行了解析,这并不是高效的做法。
在网上查阅了相关资料,了解到:
Django的queryset是惰性的,使用filter语句进行查询,实际上并没有运行任何的要真正从数据库获得数据
只要你查询的时候才真正的操作数据库。会导致执行查询的操作有:对QuerySet进行遍历queryset,切片,序列化,对QuerySet应用list()、len()方法,还有if语句
当第一次进入循环并且对QuerySet进行遍历时,Django从数据库中获取数据,在它返回任何可遍历的数据之前,会在内存中为每一条数据创建实例,而这有可能会导致内存溢出。
上面的原来很好的解释了代码所造成的现象。那么如何进行优化是个问题,网上有说到当QuerySet非常巨大时,为避免将它们一次装入内存,可以使用迭代器iterator()来处理,但对上面的代码进行修改,遍历时使用employee.iterator(),而结果和之前一样,内存持续增长,前台页面等待,对此的解释是:usingiterator()willsaveyousomememorybynotstoringtheresultofthecacheinternally(thoughnotnecessarilyonPostgreSQL!);butwillstillretrievethewholeobjectsfromthedatabase。
这里我们知道不能一次性对QuerySet中所有的记录进行遍历,那么只能对QuerySet进行切片,每次取一个chunk_size的大小,遍历这部分数据,然后进行累加,当达到需要的数目时,返回满足的对象列表,这里修改下filter_with_nation函数:
deffilter_with_nation(all_employees,nationality,num_per_page,page_num):
result=[]
pos=(page_num-1)*num_per_page
cnt=0
start_pos=0
start=False
whileTrue:
employees=all_employees[start_pos:start_pos+num_per_page]
start_pos+=num_per_page
foremployeeinemployees:
info=json.loads(employee.infomation)
ifinfo.nationality!=nationality:
continue
ifcnt==pos:
ifstart:
break
cnt=0
pos=num_per_page
start=True
ifstart:
result.append(opt)
cnt+=1
ifcnt==num_per_pageornotevents:
break
returnresult
运行上述代码时,查询的速度更快,内存也没有明显的增长,得到效果不错的优化。这篇文章初衷在于记录自己对django中queryset的理解和使用,而对于文中的例子,其实正常业务中,如果需要记录员工详细的信息,最好对employee表进行扩充,或者建立一个字表,存放详细信息,而不是将所有信息存放入一个字段中,避免在查询时的二次解析。
Django中复杂的查询在上面所有的例子中,我们构造的过滤器都只是将字段值与某个常量做比较。如果我们要对两个字段的值做比较,那该怎么做呢?
Django提供F()来做这样的比较。F()的实例可以在查询中引用字段,来比较同一个model实例中两个不同字段的值。
Django支持F()对象之间以及F()对象和常数之间的加减乘除和取模的操作。
filter()等方法中的关键字参数查询都是一起进行“AND”的。如果你需要执行更复杂的查询(例如OR语句),你可以使用Q对象。
fromdjango.db.modelsimportQ
Q(title__startswith='Py')
Q对象可以使用和|操作符组合起来。当一个操作符在两个Q对象上使用时,它产生一个新的Q对象。
查询名字叫水浒传或者价格大于100的书
你可以组合和|操作符以及使用括号进行分组来编写任意复杂的Q对象。同时,Q对象可以使用~操作符取反,这允许组合正常的查询和取反(NOT)查询:
查询函数可以混合使用Q对象和关键字参数。所有提供给查询函数的参数(关键字参数或Q对象)都将"AND”在一起。但是,如果出现Q对象,它必须位于所有关键字参数的前面。例如:
查询名字叫水浒传与价格大于100的书
结语:以上就是首席CTO笔记为大家整理的关于怎么解决django惰性查询的相关内容解答汇总了,希望对您有所帮助!如果解决了您的问题欢迎分享给更多关注此问题的朋友喔~