2010-11-19 93 views
72

問題表包含大約一千萬行。爲什麼迭代通過一個耗費大量內存的大型Django QuerySet?

for event in Event.objects.all(): 
    print event 

這會導致內存使用量穩步增加到4 GB左右,此時行可以快速打印。第一行打印之前的漫長延遲令我感到驚訝 - 我預計它幾乎立即打印。

我也試過Event.objects.iterator(),其行爲方式相同。

我不明白Django加載到內存或它爲什麼這樣做。我期望Django在數據庫級別迭代結果,這意味着結果將以大致恆定的速率打印(而不是在長時間等待後立即打印)。

我誤解了什麼?

(我不知道它是否是相關的,但我使用PostgreSQL。)

+3

在更小的機器這甚至可能會導致馬上 「封殺」 的Django的殼或服務器 – Stefano 2013-01-03 10:36:12

回答

78

Nate C很接近,但並不完全。

the docs

您可以評估在以下幾個方面一個QuerySet:

  • 迭代。 QuerySet是可迭代的,並且在您第一次迭代它時執行它的數據庫查詢。例如,這將打印的所有條目的標題數據庫:

    for e in Entry.objects.all(): 
        print e.headline 
    

所以你千萬檢索行,全部一次,當你第一次進入該循環,並得到了反覆的形式的查詢集。您遇到的等待是Django加載數據庫行併爲每個數據庫創建對象,然後再返回可以實際迭代的內容。然後,你有記憶中的一切,結果溢出。

從我閱讀的文檔中,iterator()沒有什麼比繞過QuerySet的內部緩存機制。我認爲它可能有意義做一個一個的事情,但是這反過來需要在您的數據庫上進行一千萬個單擊。也許不是所有的想法。

遍歷大型數據集的高效是我們還沒有得到完全正確,但也有一些片段在那裏爲你的目的,你會覺得非常有用:

+1

感謝偉大的答案, @eternicode。最後,我們下降到原始SQL,以獲得所需的數據庫級迭代。 – davidchambers 2011-08-13 21:15:27

+2

@eternicode很好的答案,只是碰到這個問題。從那以後,Django中有沒有相關的更新? – 2014-10-13 14:46:07

+0

Still MIA:這是一個使用光標執行此操作的版本,因此不會跳過項目...... – mlissner 2016-02-13 18:11:42

6

這是從文檔: http://docs.djangoproject.com/en/dev/ref/models/querysets/

,直到你做的東西沒有數據庫活動實際發生評估該查詢集。

因此,當運行print event時,查詢會觸發(根據您的命令進行全表掃描)並加載結果。你要求所有的對象,並且沒有辦法得到第一個對象而沒有得到所有的對象。

但是,如果你是這樣的:

Event.objects.all()[300:900] 

http://docs.djangoproject.com/en/dev/topics/db/queries/#limiting-querysets

然後,它會在內部偏移和限制添加到SQL。

5

對於大量記錄,database cursor表現更好。你確實需要Django中的原始SQL,Django-cursor與SQL cursur不同。

Nate C建議的LIMIT - OFFSET方法可能適合您的情況。對於大量的數據,它比光標慢,因爲它必須反覆運行相同的查詢,並且必須跳過越來越多的結果。

+3

Frank,這絕對是一個很好的觀點,但會很高興看到一些代碼細節可以推動解決方案;-)(以及這個問題現在已經很老了......) – Stefano 2013-01-03 10:38:05

25

可能不是更快或最有效的,但作爲一個現成的解決方案,爲什麼不使用Django核心的分頁程序,並記錄在這裏的Page對象:

https://docs.djangoproject.com/en/dev/topics/pagination/

事情是這樣的:

from django.core.paginator import Paginator 
from djangoapp.models import model 

paginator = Paginator(model.objects.all(), 1000) # chunks of 1000, you can 
               # change this to desired chunk size 

for page in range(1, paginator.num_pages + 1): 
    for row in paginator.page(page).object_list: 
     # here you can do whatever you want with the row 
    print "done processing page %s" % page 
+0

自發布以來,現在可以做出很小的改進。 'Paginator'現在有一個['page_range'](https://docs.djangoproject.com/en/dev/topics/pagination/#django.core.paginator.Paginator.page_range)屬性來避免樣板。 如果爲了尋找最小的內存開銷,可以使用['object_list.iterator()',它不會填充查詢集緩存](https://docs.djangoproject.com/en/dev/ref/models/querysets /#django.db.models.query.QuerySet.iterator)。 ['prefetch_related_objects'](https://docs.djangoproject.com/en/1.10/ref/models/querysets/#prefetch-related-objects)需要進行預取 – 2017-04-09 00:23:52

5

Django沒有很好的解決方案來從數據庫中獲取大型項目。

import gc 
# Get the events in reverse order 
eids = Event.objects.order_by("-id").values_list("id", flat=True) 

for index, eid in enumerate(eids): 
    event = Event.object.get(id=eid) 
    # do necessary work with event 
    if index % 100 == 0: 
     gc.collect() 
     print("completed 100 items") 

values_list可以用來獲取數據庫中的所有的ID,然後分別提取的每個對象。在一段時間內,大的對象將在內存中創建並且不會被垃圾收集,直到退出循環。上面的代碼在每消耗100個項目後都會手動進行垃圾收集。

+0

streamingHttpResponse是一個解決方案嗎? http://stackoverflow.com/questions/15359768/django-1-5-using-the-new-streaminghttpresponse – ratata 2014-08-14 21:59:51

+1

但是,這將導致在數據庫中的平均命中數爲循環的數量,我很擔心。 – raratiru 2016-10-08 19:22:22

4

因爲整個查詢集的這種方式對象會一次加載到內存中。你需要將你的查詢集分成較小的易消化位。做這個的模式被稱爲spoonfeeding。這是一個簡短的實施。

def spoonfeed(qs, func, chunk=1000, start=0): 
    ''' Chunk up a large queryset and run func on each item. 

    Works with automatic primary key fields. 

    chunk -- how many objects to take on at once 
    start -- PK to start from 

    >>> spoonfeed(Spam.objects.all(), nom_nom) 
    ''' 
    while start < qs.order_by('pk').last().pk: 
     for o in qs.filter(pk__gt=start, pk__lte=start+chunk): 
      func(o) 
     start += chunk 

要使用這個你寫的做你的對象上操作的功能:

def set_population_density(town): 
    town.population_density = calculate_population_density(...) 
    town.save() 

,比上運行您的查詢集該功能:

spoonfeed(Town.objects.all(), set_population_density) 

這可以進一步提高對通過多處理在多個對象上並行執行func

16

Django的默認行爲是在評估查詢時緩存QuerySet的整個結果。您可以使用查詢集的iterator方法來避免這種緩存:

for event in Event.objects.all().iterator(): 
    print event 

https://docs.djangoproject.com/en/dev/ref/models/querysets/#iterator

的iterator()方法評估查詢集,然後直接而不在查詢集級別執行緩存讀取的結果。在遍歷大量只需要訪問一次的對象時,此方法會帶來更好的性能和顯着的內存減少。請注意,緩存仍然在數據庫級別完成。

使用iterator()減少了我的內存使用量,但仍然高於我的預期。使用mpaf建議的paginator方法使用的內存要少得多,但對於我的測試用例來說,速度要慢2-3倍。

from django.core.paginator import Paginator 

def chunked_iterator(queryset, chunk_size=10000): 
    paginator = Paginator(queryset, chunk_size) 
    for page in range(1, paginator.num_pages + 1): 
     for obj in paginator.page(page).object_list: 
      yield obj 

for event in chunked_iterator(Event.objects.all()): 
    print event 
2

這裏的解決方案,包括LEN和計數:

class GeneratorWithLen(object): 
    """ 
    Generator that includes len and count for given queryset 
    """ 
    def __init__(self, generator, length): 
     self.generator = generator 
     self.length = length 

    def __len__(self): 
     return self.length 

    def __iter__(self): 
     return self.generator 

    def __getitem__(self, item): 
     return self.generator.__getitem__(item) 

    def next(self): 
     return next(self.generator) 

    def count(self): 
     return self.__len__() 

def batch(queryset, batch_size=1024): 
    """ 
    returns a generator that does not cache results on the QuerySet 
    Aimed to use with expected HUGE/ENORMOUS data sets, no caching, no memory used more than batch_size 

    :param batch_size: Size for the maximum chunk of data in memory 
    :return: generator 
    """ 
    total = queryset.count() 

    def batch_qs(_qs, _batch_size=batch_size): 
     """ 
     Returns a (start, end, total, queryset) tuple for each batch in the given 
     queryset. 
     """ 
     for start in range(0, total, _batch_size): 
      end = min(start + _batch_size, total) 
      yield (start, end, total, _qs[start:end]) 

    def generate_items(): 
     queryset.order_by() # Clearing... ordering by id if PK autoincremental 
     for start, end, total, qs in batch_qs(queryset): 
      for item in qs: 
       yield item 

    return GeneratorWithLen(generate_items(), total) 

用法:

events = batch(Event.objects.all()) 
len(events) == events.count() 
for event in events: 
    # Do something with the Event 
0

我通常使用的MySQL原生查詢,而不是Django的ORM對於這樣的任務。

MySQL支持流模式,因此我們可以安全快速地循環所有記錄而不會出現內存不足錯誤。

import MySQLdb 
db_config = {} # config your db here 
connection = MySQLdb.connect(
     host=db_config['HOST'], user=db_config['USER'], 
     port=int(db_config['PORT']), passwd=db_config['PASSWORD'], db=db_config['NAME']) 
cursor = MySQLdb.cursors.SSCursor(connection) # SSCursor for streaming mode 
cursor.execute("SELECT * FROM event") 
while True: 
    record = cursor.fetchone() 
    if record is None: 
     break 
    # Do something with record here 

cursor.close() 
connection.close() 

編號:

  1. Retrieving million of rows from MySQL
  2. How does MySQL result set streaming perform vs fetching the whole JDBC ResultSet at once