怎么解决django惰性查询?

发布网友 发布时间: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惰性查询的相关内容解答汇总了,希望对您有所帮助!如果解决了您的问题欢迎分享给更多关注此问题的朋友喔~

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com