最近做了一個小的需求,在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表進行擴充,或者建立一個字表,存放詳細信息,而不是將所有信息存放入一個字段中,避免在查詢時的二次解析。
參考:
