회사에서 비정규화를 할 일이 있었는데 데이터가 10만건이 넘어서 실 서버에 db히트 하는 만큼 성능에 신경을 써야됬습니다.
그래서 쿼리를 불러 올 때 가장 빠르게 처리하는 방식이 무엇일 지 궁금해졌습니다
.count(), annotate(Count()), len(prefetch)를 떠올렸고 비교해 봤습니다
.count()
.count()의 경우 db에서 count를 해서 가져오는 방법입니다
db에서 연산을 끝낸 후 가져오는 만큼 cpu와 메모리에는 부담이 없습니다.
.count()는 일단 조건을 사용 할 수 없습니다. 연관 된 모든 객체를 count하기 때문에 soft delete된 것이나 filter 조건을 넣을 수 없습니다. soft 삭제를 구현하고 있는 테이블이라 필터 조건이 필요한 만큼 사용 할 수는 없었습니다
.annotate()
db에서 새로운 칼럼을 만들어서 가져오는 방식입니다
count()와 비슷하게 db에서 연산을 통해서 가져오기에 cpu와 메모리에 부담은 없습니다
그리고 추가적인 필터를 설정 할 수 있기 때문에 원하는 조건을 설정해 정확한 값을 넣을 수 있습니다.
.prefetch() + len()
db에서 쿼리를 하나 더 가져오는 방식입니다.
데이터를 가져와 cpu와 메모리에서 처리하기 때문에 서버에 부담은 갈 수 있습니다.
속도적인 측면에서 비교 결과
just import
annotate test:
Execution time: 1.29 seconds
count test:
Execution time: 50.27 seconds
prefetch test:
execution time: 21.42 seconds
======
save()
annotate test:
Execution time: 65.27 seconds
count test:
Execution time: 51.75 seconds
prefetch test:
Total execution time: 35.59 seconds
prefetch가 더 뛰어난 속도를 보였습니다
(save()를 하지 않고 단순히 값을 가져오는 것은 annotate가 더 빨랐습니다)
왜 단순히 import 하는건 annotation이 더 빠른데 값을 save()하는 것은 느릴까에 대해 의문점이 생겼습니다.
정확한 답은 찾지 못했지만 예상으로 아래의 결과가 나왔습니다.
- db의 연산 속도가 서버보다 더 느리다 => 그러면 가져올 때도 느려야 하지 않는가?
- DTO로 값을 들고오는 것은 빠르지만 객체에서 값을 빼서 사용하려면 느리다 (유력)
추 후에 더 좋은 이유를 알아내면 업데이트 하도록 하겠습니다
- 단순 값을 가져오려면 annotation이 더 빠르다
- 값을 저장 하려면 prefetch가 더 좋다
- .count()는 커스텀 하기 어렵다
비교 코드와 완성 코드 첨부
#batch_size를 통해서 쿼리 히트는 많아지지만 안정성 높힘
def set_like_place_prefetch_with_batch_size(batch_size=500):
start_time = time.time()
total_places_count = Place.objects.filter(removed_at__isnull=True).count()
print(f'Total {total_places_count} queries')
for start_index in range(0, total_places_count, batch_size):
batch_start_time = time.time()
end_index = start_index + batch_size if start_index + \
batch_size < total_places_count else total_places_count
place_query = Place.objects.filter(removed_at__isnull=True).prefetch_related(
models.Prefetch(
'place_likers',
queryset=LikingPlace.objects.filter(removed_at__isnull=True),
to_attr='_place_likers',
)
)[start_index:end_index]
print(f'Processing {start_index} ~ {end_index}')
with transaction.atomic():
for place in place_query:
place.liking_count = len(place._place_likers)
place.save()
batch_end_time = time.time()
print(f' - Batch processing time: {batch_end_time - batch_start_time} seconds')
end_time = time.time()
print(f"Total elapsed time: {end_time - start_time} seconds")
#속도 비교 코드
@transaction.atomic()
def set_default_like_place_annotate(start, end):
start_time = time.time()
print(f"Start time: {start_time}")
place_query = Place.objects.filter(removed_at__isnull=True).annotate(
exist_liking_count=models.Count('place_likers', filter=Q(place_likers__removed_at__isnull=True)))[start:end]
for place in place_query:
place.liking_count = place.exist_liking_count
place.save()
end_time = time.time()
elapsed_time = end_time - start_time
print(f"Execution time: {elapsed_time:.2f} seconds")
@transaction.atomic()
def set_default_like_place_prefetch_batch_size(start, end):
start_time = time.time()
print(f"Start time: {start_time}")
place_query = Place.objects.filter(removed_at__isnull=True).prefetch_related(
models.Prefetch(
'place_likers',
queryset=LikingPlace.objects.filter(removed_at__isnull=True),
to_attr='_place_likers',
)
)[start:end]
for place in place_query:
place.liking_count = len(place._place_likers)
place.save()
end_time = time.time()
print(f"End time: {end_time}")
print(f"Total execution time: {end_time - start_time:.2f} seconds")
def set_default_like_place_count(start, end):
start_time = time.time()
print(f"Start time: {start_time}")
with transaction.atomic():
place_query = Place.objects.filter(removed_at__isnull=True)[start:end]
for place in place_query:
if place:
place_likers_count = place.place_likers.count()
place.liking_count = place_likers_count
place.save()
end_time = time.time() # 종료 시간 측정
elapsed_time = end_time - start_time # 경과 시간 계산
print(f"Execution time: {elapsed_time:.2f} seconds")
'Django > orm' 카테고리의 다른 글
update_or_create (0) | 2024.06.28 |
---|---|
Dynamdb() Scan vs query (0) | 2024.02.06 |
DynamoDB() (0) | 2024.02.05 |
Django date range (0) | 2024.02.01 |
F() (0) | 2024.01.24 |