[sql] 큰 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


답변

이것은 문서에서 가져온 것입니다 :
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에 오프셋과 제한을 추가합니다.


답변

많은 양의 레코드의 경우 데이터베이스 커서가 더 잘 수행됩니다. Django에서 원시 SQL이 필요합니다. Django 커서는 SQL cursur와 다른 것입니다.

Nate C가 제안한 LIMIT-OFFSET 방법이 귀하의 상황에 충분할 수 있습니다. 많은 양의 데이터의 경우 동일한 쿼리를 반복해서 실행해야하고 더 많은 결과를 건너 뛰어야하기 때문에 커서보다 느립니다.


답변

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를 가져온 다음 각 개체를 개별적으로 가져 오는 데 사용할 수 있습니다. 시간이 지남에 따라 큰 개체가 메모리에 생성되고 for 루프가 종료 될 때까지 가비지 수집되지 않습니다. 위의 코드는 매 100 번째 항목이 소비 된 후 수동 가비지 수집을 수행합니다.


답변

그런 식으로 전체 쿼리 세트의 개체가 한 번에 메모리에로드되기 때문입니다. 쿼리 세트를 더 작은 소화 가능한 비트로 청크해야합니다. 이를 수행하는 패턴을 숟가락 수유라고합니다. 다음은 간단한 구현입니다.

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

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

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

쿼리 세트에서 해당 함수를 실행하는 것보다

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

이것은 func여러 객체에서 병렬 로 실행 되는 다중 처리를 통해 더욱 향상 될 수 있습니다 .