最近作了一個小的需求,在django模型中經過前臺頁面的表單的提交(post),後臺對post的參數進行解析,經過models模型查詢MySQL,將數據結構進行加工,返回到前臺頁面進行展現。因爲對django中QuerySet特性的不熟悉,因此測試過程當中發現了不少問題。html
開始的階段沒有遇到什麼問題,咱們舉例,在models有一張員工表employee,對應的表結構中,postion列表示員工職位,前臺post過來的參數賦給position,加上入職時間、離職時間,查詢操做經過models.filter(position=params)完成,獲取的員工信息內容由QuerySet和當前展現頁與每頁展現的記錄數進行簡單的計算,返回給前臺頁面進行渲染展現。編碼以下:mongodb
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中的數據,而且對每條數據進行了解析,這並非高效的作法。django
在網上查閱了相關資料,瞭解到:json
1 Django的queryset是惰性的,使用filter語句進行查詢,實際上並無運行任何的要真正從數據庫得到數據數據結構
2 只要你查詢的時候才真正的操做數據庫。會致使執行查詢的操做有:對QuerySet進行遍歷queryset,切片,序列化,對 QuerySet 應用 list()、len()方法,還有if語句app
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。post
這裏咱們知道不能一次性對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表進行擴充,或者創建一個字表,存放詳細信息,而不是將全部信息存放入一個字段中,避免在查詢時的二次解析。
參考: