큰 Django QuerySet을 반복하는 데 많은 양의 메모리가 소비되는 이유는 무엇입니까?


111

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

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

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

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

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

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

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


6
더 작은 기계에서는 이로 인해 django 쉘 또는 서버에 즉시 "Killed"가 발생할 수 있습니다
Stefano

답변:


113

Nate C는 가까웠지만 정답은 아니 었습니다.

에서 워드 프로세서 :

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

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

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

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

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

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


1
@eternicode의 훌륭한 답변에 감사드립니다. 결국 우리는 원하는 데이터베이스 수준 반복을 위해 원시 SQL로 떨어졌습니다.
davidchambers 2011-08-13

2
@eternicode 좋은 대답, 방금이 문제를 누르십시오. 그 이후로 장고에 관련된 업데이트가 있습니까?
Zólyomi István

2
Django 1.11 이후의 문서에서는 iterator ()가 서버 측 커서를 사용한다고 말합니다.
Jeff C Johnson

42

더 빠르거나 효율적이지 않을 수도 있지만, 준비된 솔루션으로 여기에 문서화 된 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

3
포스트 이후로 이제 작은 개선이 가능합니다. Paginator이제 page_range상용구를 피하는 속성이 있습니다. 최소한의 메모리 오버 헤드를 찾고 있다면 object_list.iterator()쿼리 셋 캐시를 채우지 않는 것을 사용할 수 있습니다 . prefetch_related_objects그런 다음 프리 페치에 필요합니다
Ken Colton 2017

28

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

8

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


7

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

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


4
확실히 좋은 점이다 그러나 해결책으로 조금씩 이동하는 몇 가지 코드 세부 사항을 볼 수 좋을 것이다 프랭크, ;-) (물론이 질문은 ... 지금은 아주 오래된)
스테파노

7

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


streamingHttpResponse가 솔루션이 될 수 있습니까? stackoverflow.com/questions/15359768/…
ratata 2014-08-14

2
그러나 이것은 루프 수와 데이터베이스에서 동일한 히트를 초래할 것입니다.
raratiru

5

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

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여러 객체에서 병렬 로 실행 되는 다중 처리를 통해 더욱 향상 될 수 있습니다 .


1
이 같은 외모가 반복 처리로 1.12에 내장 될 것입니다 (수 chunk_size = 1000)
케빈 파커

3

다음은 len 및 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)

용법:

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

0

나는 일반적으로 이런 종류의 작업에 Django 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. MySQL 결과 세트 스트리밍은 전체 JDBC ResultSet을 한 번에 가져 오는 것과 어떻게 수행합니까?

Django ORM을 사용하여 쿼리를 생성 할 수 있습니다. 결과 queryset.query를 사용 하여 실행하십시오.
Pol
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.