Common Error in QuerySet

Jayson Hwang·2022년 7월 26일
1
post-thumbnail

실수하기 쉬운 QuerySet 특성

인턴십 간 프로젝트를 진행하며 효율적이고 실행시간을 단축시킬 수 있는 전략을 많이 적용하고자, Django ORM QuerySet이 지닌 특성들을 활용하여 코드를 작성하였습니다. 하지만, 당연히 제가 의도한대로 잘 수행되고 있다고 생각했던 코드들이 사실은 작동하지 않은 죽은 코드들이었다는 사실을 깨닳았습니다.

이번 포스팅에서는 제가 작성한 코드가 "왜 의도한대로 수행되지 않을 수 밖에 없었는 지"에 대해서 놓치기 쉬운 QuerySet의 특성 (values()를 사용하는 경우에는 Eager-Loading 옵션을 전부 무시하는 특성)을 정리해가며 공부한 내용을 다루어보겠습니다.



왜 코드가 의도한대로 작동하지 않지?

우선 제가 작성한 코드는 아래와 같습니다.

doctors = Doctor.objects.select_related('user', 'department', 'hospital').filter(department_id=department_id)\
        .annotate(
            doctor_name        = F('user__name'),
            doctor_department  = F('department__name'),
            doctor_hospital    = F('hospital__name'),
            doctor_profile_img = Concat(V(f'{settings.LOCAL_PATH}/doctor_profile_img/'), 'profile_img', output_field=CharField())
        ).values('id', 'doctor_name', 'doctor_department', 'doctor_hospital', 'doctor_profile_img').order_by('id')

위와 같이 코드를 작성한 이유는 다음과 같습니다.

첫번째 이유는 Eager-Loading 전략을 사용하여 select_related 메소드로 JOIN 문(SQL 단계에서의 JOIN)을 통하여 데이터를 즉시 로딩할 수 있도록 하기 위함입니다.

두번째 이유는 궁극적으로 쿼리 수를 줄여 데이터베이스 동작시간을 줄이기 위함입니다. DB에 엑세스하는 횟수를 줄여줌으로써 Performance를 향상시켜줄 수 있다고 생각했습니다.

하지만, 얼마나 쿼리 횟수가 줄어드는 지 확인을 해보기 위해서 query_debugger 데코레이터connection.queries을 통해서 확인했으나 select_related를 사용한 경우와 사용하지 않은 경우가 차이가 없음을 확인하였습니다.

(select_related를 사용한 경우와 사용하지 않은 경우 실행되는 쿼리 횟수에 차이가 없다???)

도대체 이유가 뭐야 ??

계속해서 정보를 찾아나가던 중 "PyconKorea 2020 영상" 에서 이유를 찾았습니다.

결론부터 말씀드리면, QuerySet에는values(), values_list()를 사용하는 경우에는 Eager-Loading 옵션인 select_related(), prefetch_related()를 전부 무시하는 특성이 있습니다.


아래에 직접 실험한 예제를 보도록 하겠습니다.

[[ select_related 무시 ]]

>>> doctor_queryset = Doctor.objects.select_related('user', 'department', 'hospital').filter(id=1).values()
>>>
>>> doctor_queryset
SELECT `doctors`.`id`, `doctors`.`user_id`, `doctors`.`department_id`, `doctors`.`hospital_id`, `doctors`.`profile_img`
FROM `doctors`
WHERE `doctors`.`id` = 1
LIMIT 21; args=(1,); alias=default

# select_related를 무시하고 JOIN 하지 않는 것을 확인할 수 있다.
# 그리고 DB의 row 단위로 데이터를 가져온다.
<QuerySet [{'id': 1, 'user_id': 81, 'department_id': 1, 'hospital_id': 1, 'profile_img': 'profile1.png'}]>




[[ JOIN이 이루어진 경우 ]]

>>> doctor_queryset = Doctor.objects.select_related('user', 'department', 'hospital').filter(id=1).values(F'user__name')
>>>
>>> doctor_queryset
SELECT `users`.`name`
FROM `doctors`
  INNER JOIN `users` ON (`doctors`.`user_id` = `users`.`id`)
WHERE `doctors`.`id` = 1
LIMIT 21; args=(1,); alias=default

# values()안에 (F'users__name')를 명시해주어서 JOIN이 된 것을 확인할 수 있다.
# 다시 말하면, JOIN해야만 가져올 수 있는 데이터를 명시해줘야 JOIN이 된다.
<QuerySet [{'user__name': '권혁수'}]>

예제와 같이 values() 의 경우 DB의 Row 단위로 데이터를 반환합니다.

이 말은 즉슨, values() 를 사용하는 케이스는 그냥 관계지향(Relational)적인 데이터의 리스트를 그냥 반환해달라고 하는 것입니다.

values()를 사용하면 ORM 특성인 객체(Object)와 관계지향(Relational)간에는 매핑(Mapping)이 일어나지 않게 됩니다.

따라서 ORM의 Eager-Loading이라는 개념(추가 모델들을 한번에 가져오기 위해 사용)을 구현하기위해 사용되는 메소드인 select_relatedprefetch_relatedvalues()와 함께 사용되면 당연히 무의미한 옵션이 되어버릴 수 밖에 없습니다.



실제로는 얼마나 차이가 날까 ??

실제로 데이터가 많을 경우에는 .annotate() + values()를 사용한 경우List Comprehension + Eager-Loading 을 사용한 경우가 어떤 방법이 얼마나 효율적일지 궁금해졌습니다.

인턴십 기간 중 찾아내고 공부한 내용이라 사수분께 질문드리며 해당 내용에 대해서 공유했고,
사수님께서 직접 개발서버에서 비교를 해주셨습니다.

"결과"는 List Comprehension 보다 values()를 사용하는 방법이 0.0002 vs 0.0005로 2배 조금 넘게 효율적이라는 결과가 나왔습니다.

다만, 사수님께서 말씀해주시길 고려해야할 사항으로 해당 개발 서버에는 Table에 Column의 수가 많았으며, filter 조건에 따라서도 차이가 날 수도 있고, 결론적으로는 WHERE 로 걸러지는 것이 몇 개인지에 따라서 속도 차이가 날 것이라 말씀해주셨습니다.



마치며

ORM으로 항상 복잡한 SQL을 구현했다고 해서 ORM을 잘 쓰는 것은 아니며,
실제로 본인이 의도를 가지고 작성한 코드가 오류없이 실행된다고해서 100% 의도대로 실행되고 있지 않을 수도 있다는 사실을 깨닳았습니다.

직접 모델을 만들고 데이터를 넣어보고 Shell을 통해 ORM을 작성해보면서 공부한다면 QuerySet를 이해하고 더욱 효율적으로 사용할 수 있을 것이라 생각합니다.

profile
"Your goals, Minus your doubts, Equal your reality"

0개의 댓글