• 如何有效的遍历django的QuerySet


      最近做了一个小的需求,在django模型中通过前台页面的表单的提交(post),后台对post的参数进行解析,通过models模型查询MySQL,将数据结构进行加工,返回到前台页面进行展示。由于对django中QuerySet特性的不熟悉,所以测试过程中发现了很多问题。

      开始的阶段没有遇到什么问题,我们举例,在models有一张员工表employee,对应的表结构中,postion列表示员工职位,前台post过来的参数赋给position,加上入职时间、离职时间,查询操作通过models.filter(position=params)完成,获取的员工信息内容由QuerySet和当前展示页与每页展示的记录数进行简单的计算,返回给前台页面进行渲染展示。编码如下:

     1 def get_employees(position, start, end):
     2     return employee.objects.filter(alert_time__lt=end,alert_time__gt=start).filter(position__in=position)
     3     
     4 
     5 @login_required
     6 def show(request):
     7     if not validate(request):
     8         return render_to_response('none.html', 
     9                                   context_instance=RequestContext(request, 'msg':'params error')
    10                                   )
    11         
    12     position = request.REQUEST.get('position')
    13     time_range = request.REQUEST.get('time')
    14     start, end = time_range[0], time_range[1]
    15     
    16     num_per_page, page_num = get_num(request)
    17     all_employees = get_employees(position, start, end)
    18   # 根据当前页与每页展示的记录数,取到正确的记录
    19     employees = employees_events[(page_num-1)*num_per_page:page_num*num_per_page]
    20     
    21     return render_to_response('show_employees.html',
    22                               context_instance=RequestContext(
    23                                   request,
    24                                   'employees': employees,
    25                                   'num_per_page': num_per_page,
    26                                   'page_num':page_num,
    27                                   'page_options' : [50, 100, 200]
    28                               )
    29                             )

      运行之后可以正确的对所查询的员工信息进行展示,并且查询速度很快。employee表中存放着不同职位的员工信息,不同类型的详细内容也不相同,假设employees有一列名为infomation,存储的是员工的详细信息,infomation = {'age': 33, 'gender': 'male', 'nationality': 'German', 'degree': 'doctor', 'motto': 'just do it'},现在的需求是要展示出分类更细的员工信息,前台页面除了post职位、入职离职时间外,还会对infomation中的内容进行筛选,这里以查询中国籍的设计师为例,在之前的代码基础上,需要做一些修改。员工信息表employee存放于MySQL中,而MySQL为ORM数据库,它并未提供类似mongodb一样更为强大的聚合函数,所以这里不能通过objects提供的方法进行filter,一次性将所需的数据获取出来,那么需要对type进行过滤后的数据,进行二次遍历,通过information来确定当前记录是否需要返回展示,在展示过程中,需要根据num_per_page和page_num计算出需要展示数据起始以及终止位置。

     1 def get_employees(position, start, end):
     2     return employee.objects.filter(alert_time__lt=end,alert_time__gt=start).filter(position__in=position)
     3     
     4 
     5 def filter_with_nation(all_employees, nationality, num_per_page, page_num):
     6     result = []
     7 
     8     pos = (page_num-1)*num_per_page
     9     cnt = 0
    10     start = False
    11     for employee in all_employees:
    12         info = json.loads(employee.information)
    13         if info.nationality != nationality:
    14             continue
    15 
    16         # 获取的数据可能并不是首页,所以需要先跳过前n-1页
    17         if cnt == pos:
    18             if start:
    19                 break
    20             cnt = 0
    21             pos = num_per_page
    22             start = True
    23         
    24         if start:
    25             result.append(employee)
    26             
    27     return employee
    28 
    29     
    30 @login_required
    31 def show(request):
    32     if not validate(request):
    33         return render_to_response('none.html', 
    34                                   context_instance=RequestContext(request, 'msg':'params error')
    35                                   )
    36         
    37     position = request.REQUEST.get('position')
    38     time_range = request.REQUEST.get('time')
    39     start, end = time_range[0], time_range[1]
    40     
    41     num_per_page, page_num = get_num(request)
    42     all_employees = get_employees(position, start, end)
    43     
    44     nationality = request.REQUEST.get('nationality')
    45 
    46     employees = filter_with_nation(all_employees, num_per_page, page_num)
    47     
    48     return render_to_response('show_employees.html',
    49                               context_instance=RequestContext(
    50                                   request,
    51                                   'employees': employees,
    52                                   'num_per_page': num_per_page,
    53                                   'page_num':page_num,
    54                                   'page_options' : [50, 100, 200]
    55                               )
    56                             )

      当编码完成之后,在数据employee表数据很小的情况下测试并未发现问题,而当数据量非常大,并且查询的数据很少时,代码运行非常耗时。我们设想,这是一家规模很大的跨国公司,同时人员的流动量也很大,所以employee表的数据量很庞大,而这里一些来自于小国家的员工并不多,比如需要查询国籍为梵蒂冈的员工时,前台页面进入了无尽的等待状态。同时,监控进程的内存信息,发现进程的内存一直在增长。毫无疑问,问题出现在filter_with_nation这个函数中,这里逐条遍历了employee中的数据,并且对每条数据进行了解析,这并不是高效的做法。

      在网上查阅了相关资料,了解到:

    1 Django的queryset是惰性的,使用filter语句进行查询,实际上并没有运行任何的要真正从数据库获得数据

    2 只要你查询的时候才真正的操作数据库。会导致执行查询的操作有:对QuerySet进行遍历queryset,切片,序列化,对 QuerySet 应用 list()、len()方法,还有if语句

    3 当第一次进入循环并且对QuerySet进行遍历时,Django从数据库中获取数据,在它返回任何可遍历的数据之前,会在内存中为每一条数据创建实例,而这有可能会导致内存溢出。

      上面的原来很好的解释了代码所造成的现象。那么如何进行优化是个问题,网上有说到当QuerySet非常巨大时,为避免将它们一次装入内存,可以使用迭代器iterator()来处理,但对上面的代码进行修改,遍历时使用employee.iterator(),而结果和之前一样,内存持续增长,前台页面等待,对此的解释是:using iterator() will save you some memory by not storing the result of the cache internally (though not necessarily on PostgreSQL!); but will still retrieve the whole objects from the database。

      这里我们知道不能一次性对QuerySet中所有的记录进行遍历,那么只能对QuerySet进行切片,每次取一个chunk_size的大小,遍历这部分数据,然后进行累加,当达到需要的数目时,返回满足的对象列表,这里修改下filter_with_nation函数:

     1 def filter_with_nation(all_employees, nationality, num_per_page, page_num):
     2     result = []
     3 
     4     pos = (page_num-1)*num_per_page
     5     cnt = 0
     6     start_pos = 0
     7     start = False
     8     while True:
     9         employees = all_employees[start_pos:start_pos+num_per_page]
    10         start_pos += num_per_page
    11  
    12         for employee in employees:
    13             info = json.loads(employee.infomation)
    14             if info.nationality != nationality:
    15                 continue
    16  
    17             if cnt == pos:
    18                 if start:
    19                     break
    20                 cnt = 0
    21                 pos = num_per_page
    22                 start = True
    23  
    24             if start:
    25                 result.append(opt)
    26  
    27             cnt += 1
    28  
    29         if cnt == num_per_page or not events:
    30             break
    31             
    32     return result

       运行上述代码时,查询的速度更快,内存也没有明显的增长,得到效果不错的优化。这篇文章初衷在于记录自己对django中queryset的理解和使用,而对于文中的例子,其实正常业务中,如果需要记录员工详细的信息,最好对employee表进行扩充,或者建立一个字表,存放详细信息,而不是将所有信息存放入一个字段中,避免在查询时的二次解析。


      参考:

      http://www.oschina.net/translate/django-querysets

      http://stackoverflow.com/questions/4222176/why-is-iterating-through-a-large-django-queryset-consuming-massive-amounts-of-me

  • 相关阅读:
    批处理学习笔记9
    批处理学习笔记8
    批处理学习笔记10
    批处理学习笔记7
    批处理学习笔记6
    批处理学习笔记系列
    批处理学习笔记5
    批处理学习笔记3
    批处理学习笔记4
    批处理学习笔记2
  • 原文地址:https://www.cnblogs.com/Tour/p/5028093.html
Copyright © 2020-2023  润新知