your programing

왜 큰 장고 쿼리 세트를 통해 반복하는 것이 막대한 양의 메모리를 소비합니까?

lovepro 2023. 5. 22. 22:24
반응형

왜 큰 장고 쿼리 세트를 통해 반복하는 것이 막대한 양의 메모리를 소비합니까?

문제의 표에는 대략 천만 줄이 있습니다.

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

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

저도 노력했습니다.Event.objects.iterator()같은 방식으로 행동했던 것.

나는 장고가 메모리에 무엇을 로딩하고 있는지, 왜 이것을 하는지 이해할 수 없습니다.저는 장고가 데이터베이스 수준에서 결과를 반복할 것으로 예상했습니다. 이는 결과가 오랜 기다림 끝에 한꺼번에 인쇄되는 것이 아니라 거의 일정한 속도로 인쇄된다는 것을 의미합니다.

제가 뭘 잘못 이해했나요?

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

네이트 C는 가까웠지만, 완전히 그렇지는 않았습니다.

문서에서:

다음과 같은 방법으로 쿼리 세트를 평가할 수 있습니다.

  • 반복.쿼리 세트는 반복 가능하며, 처음 쿼리를 반복할 때 데이터베이스 쿼리를 실행합니다.예를 들어, 데이터베이스에 있는 모든 항목의 제목이 인쇄됩니다.

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

그래서 당신이 처음 그 루프에 들어가서 쿼리 세트의 반복 형태를 얻었을 때, 당신의 천만 행이 한꺼번에 검색됩니다.실제로 반복할 수 있는 것을 반환하기 전에 데이터베이스 행을 로드하고 각 행에 대한 개체를 만드는 Django의 대기 시간입니다.그러면 여러분은 모든 것을 기억하게 되고, 결과는 쏟아져 나옵니다.

제가 문서를 읽어본 바로는 QuerySet의 내부 캐싱 메커니즘을 우회하는 것에 불과합니다.제 생각에는 하나씩 하는 것이 말이 되겠지만, 반대로 데이터베이스에 천만 번의 개별 검색이 필요할 것입니다.그렇게 바람직한 것은 아닐 수도 있습니다.

대규모 데이터셋을 효율적으로 반복하는 것은 아직 제대로 이해하지 못했지만 다음과 같은 몇 가지 정보가 있습니다.

더 빠르거나 가장 효율적이지는 않을 수도 있지만, 여기에 문서화된 djangocore의 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/stable/ref/models/querysets/ #reiterator

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

반복기()를 사용하면 메모리 사용량이 줄어들지만 여전히 예상보다 높습니다.mpf에서 제안한 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

많은 양의 레코드의 경우 데이터베이스 커서의 성능이 훨씬 뛰어납니다.장고에는 원시 SQL이 필요합니다. 장고 커서는 SQL 커서와 다릅니다.

네이트 C가 제안한 LIMIT - OFFSET 방법이 당신의 상황에 충분히 적합할 수 있습니다.대량의 데이터의 경우 동일한 쿼리를 반복적으로 실행해야 하고 점점 더 많은 결과를 건너뛰어야 하기 때문에 커서보다 속도가 느립니다.

장고는 데이터베이스에서 큰 항목을 가져올 수 있는 좋은 솔루션이 없습니다.

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번째 항목이 소비된 후 수동 가비지 수집을 수행합니다.

이것은 문서에서 온 것입니다: http://docs.djangoproject.com/en/dev/ref/models/querysets/

쿼리 집합을 평가하기 위한 작업을 수행할 때까지 데이터베이스 활동은 실제로 수행되지 않습니다.

래서그 ▁the 때.print event는 쿼리 화재(명령에 따라 전체 테이블 검색)를 실행하고 결과를 로드합니다.당신은 모든 물체를 요구하고 모든 물체를 얻지 않고는 첫 번째 물체를 얻을 수 없습니다.

하지만 다음과 같은 작업을 수행할 경우:

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

http://docs.djangoproject.com/en/dev/topics/db/queries/ #쿼리 집합 제한

그런 다음 내부적으로 SQL에 오프셋과 한계를 추가합니다.

전체 쿼리에 대한 모든 데이터베이스 행이 한 번에 개체로 처리되므로 쿼리 집합을 반복하기 전에 대량의 메모리가 사용되며, 행 수에 따라 많은 처리가 필요할 수 있습니다.

쿼리 세트를 더 작은 소화 가능 비트로 청크할 수 있습니다.저는 이 패턴을 "숟가락 먹이기"라고 부릅니다.먼저 관리 명령에서 사용하는 진행률 표시줄이 있는 구현입니다.pip3 install tqdm

from tqdm import tqdm


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)
    """
    end = qs.order_by('pk').last()
    progressbar = tqdm(total=qs.count())
    if not end:
        return
    while start < end.pk:
        for o in qs.filter(pk__gt=start, pk__lte=start+chunk):
            func(o)
            progressbar.update(1)
        start += chunk
    progressbar.close()

이를 사용하려면 개체에 대한 작업을 수행하는 함수를 작성합니다.

def set_population(town):
    town.population = calculate_population(...)
    town.save()

쿼리 세트에서 해당 기능을 실행합니다.

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

여기에는 시대에 뒤떨어진 결과들이 많이 있습니다.언제 추가되었는지는 확실하지 않지만, 장고의.QuerySet.iterator()메소드는 청크 크기의 서버측 커서를 사용하여 데이터베이스에서 결과를 스트리밍합니다.따라서 우편물을 사용하는 경우에는 이를 즉시 처리해야 합니다.

렌과 카운트를 포함한 솔루션:

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

이런 종류의 작업은 주로 장고 ORM 대신 원시 MySQL 원시 쿼리를 사용합니다.

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. MySQL에서 수백만 개의 행 검색
  2. 전체 JDBC ResultSet을 한 번에 가져오는 것과 비교하여 MySQL ResultSet 스트리밍을 수행하는 방법

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

반응형