왜 큰 장고 쿼리 세트를 통해 반복하는 것이 막대한 양의 메모리를 소비합니까?
문제의 표에는 대략 천만 줄이 있습니다.
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()
참조:
언급URL : https://stackoverflow.com/questions/4222176/why-is-iterating-through-a-large-django-queryset-consuming-massive-amounts-of-me
'your programing' 카테고리의 다른 글
출력 형식을 지정하기 위해 f-string에서 new line '\n'을 사용하려면 어떻게 해야 합니까? (0) | 2023.05.22 |
---|---|
msysgit 오른쪽 클릭 메뉴 옵션을 제거하려면 어떻게 해야 합니까? (0) | 2023.05.22 |
통합 윈도우즈 인증을 사용하여 로그인 프롬프트 수신 (0) | 2023.05.22 |
리눅스 복사는 하되 덮어쓰지 않는 방법은? (0) | 2023.05.22 |
Bash에서 한 문자를 다른 문자로 바꿉니다. (0) | 2023.04.22 |