Programing

왜 큰 Django QuerySet을 반복하면 엄청난 양의 메모리를 사용합니까?

lottogame 2020. 8. 23. 09:37
반응형

왜 큰 Django QuerySet을 반복하면 엄청난 양의 메모리를 사용합니까?


문제의 테이블에는 대략 천만 개의 행이 있습니다.

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

이로 인해 메모리 사용량이 4GB 정도까지 꾸준히 증가하여 행이 빠르게 인쇄됩니다. 첫 번째 행이 인쇄되기까지 오랜 시간이 지연되어 놀랐습니다. 거의 즉시 인쇄 될 것으로 예상했습니다.

나는 또한 Event.objects.iterator()같은 방식으로 행동하는 것을 시도했습니다 .

Django가 메모리에로드하는 것이 무엇인지 또는 왜이 작업을 수행하는지 이해하지 못합니다. Django가 데이터베이스 수준에서 결과를 반복 할 것으로 예상했는데, 이는 결과가 대략 일정한 속도로 인쇄된다는 것을 의미합니다 (긴 기다린 후 한 번에 모두 인쇄하는 것이 아님).

내가 무엇을 오해 했습니까?

(관련성이 있는지는 모르겠지만 PostgreSQL을 사용하고 있습니다.)


Nate C는 가까웠지만 그렇지 않았습니다.

에서 워드 프로세서 :

다음과 같은 방법으로 QuerySet을 평가할 수 있습니다.

  • 되풀이. QuerySet은 반복 가능하며 처음 반복 할 때 데이터베이스 쿼리를 실행합니다. 예를 들어, 이것은 데이터베이스에있는 모든 항목의 헤드 라인을 인쇄합니다.

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

따라서 처음 해당 루프를 입력하고 쿼리 세트의 반복 형식을 얻을 때 천만 개의 행이 한 번에 검색됩니다. 당신이 경험하는 기다림은 Django가 데이터베이스 행을로드하고 실제로 반복 할 수있는 것을 반환하기 전에 각 행에 대한 객체를 만드는 것입니다. 그런 다음 모든 것을 메모리에 저장하고 결과가 쏟아집니다.

문서를 읽은 후 iterator()QuerySet의 내부 캐싱 메커니즘을 우회하는 것 이상을 수행하지 않습니다. 일대일로하는 것이 합리적이라고 생각하지만, 반대로 데이터베이스에서 천만 건의 개별 적중이 필요합니다. 그다지 바람직하지 않을 수도 있습니다.

대규모 데이터 세트를 효율적으로 반복하는 것은 여전히 ​​옳지 않은 일이지만, 귀하의 목적에 유용 할 수있는 몇 가지 스 니펫이 있습니다.


더 빠르거나 가장 효율적이지 않을 수도 있지만 기성 솔루션으로 여기에 문서화 된 django 코어의 Paginator 및 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

Django의 기본 동작은 쿼리를 평가할 때 QuerySet의 전체 결과를 캐시하는 것입니다. 이 캐싱을 피하기 위해 QuerySet의 반복기 메서드를 사용할 수 있습니다.

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

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

iterator () 메서드는 queryset을 평가 한 다음 QuerySet 수준에서 캐싱을 수행하지 않고 결과를 직접 읽습니다. 이 방법을 사용하면 한 번만 액세스하면되는 많은 수의 개체를 반복 할 때 성능이 향상되고 메모리가 크게 감소합니다. 캐싱은 여전히 ​​데이터베이스 수준에서 수행됩니다.

iterator ()를 사용하면 메모리 사용량이 줄어들지 만 예상보다 여전히 높습니다. mpaf가 제안한 페이지 지정자 접근 방식을 사용하면 메모리가 훨씬 적게 사용되지만 테스트 사례에서는 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

This is from the docs: http://docs.djangoproject.com/en/dev/ref/models/querysets/

No database activity actually occurs until you do something to evaluate the queryset.

So when the print event is run the query fires (which is a full table scan according to your command.) and loads the results. Your asking for all the objects and there is no way to get the first object without getting all of them.

But if you do something like:

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

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

Then it will add offsets and limits to the sql internally.


For large amounts of records, a database cursor performs even better. You do need raw SQL in Django, the Django-cursor is something different than a SQL cursur.

The LIMIT - OFFSET method suggested by Nate C might be good enough for your situation. For large amounts of data it is slower than a cursor because it has to run the same query over and over again and has to jump over more and more results.


Django doesn't have good solution for fetching large items from database.

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 can be used to fetch all the ids in the databases and then fetch each object separately. Over a time large objects will be created in memory and won't be garbage collected til for loop is exited. Above code does manual garbage collection after every 100th item is consumed.


Because that way objects for a whole queryset get loaded in memory all at once. You need to chunk up your queryset into smaller digestible bits. The pattern to do this is called spoonfeeding. Here's a brief implementation.

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):
            yeild func(o)
        start += chunk

To use this you write a function that does operations on your object:

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

and than run that function on your queryset:

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

This can be further improved on with multiprocessing to execute func on multiple objects in parallel.


Here a solution including len and count:

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)

Usage:

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

I usually use raw MySQL raw query instead of Django ORM for this kind of task.

MySQL supports streaming mode so we can loop through all records safely and fast without out of memory error.

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()

Ref:

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

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

반응형