Django/orm

count() 관련 성능 비교

두잇 두두 2024. 7. 22. 10:02
728x90

회사에서 비정규화를 할 일이 있었는데 데이터가 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()하는 것은 느릴까에 대해 의문점이 생겼습니다.

정확한 답은 찾지 못했지만 예상으로 아래의 결과가 나왔습니다.

  1. db의 연산 속도가 서버보다 더 느리다 => 그러면 가져올 때도 느려야 하지 않는가?
  2. DTO로 값을 들고오는 것은 빠르지만 객체에서 값을 빼서 사용하려면 느리다 (유력)

추 후에 더 좋은 이유를 알아내면 업데이트 하도록 하겠습니다

 

  1. 단순 값을 가져오려면 annotation이 더 빠르다
  2. 값을 저장 하려면 prefetch가 더 좋다
  3. .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")