dev/backend

Django 1+N problem

정요한 2024. 2. 15. 17:58

>> 문제 인식

- 쿼리 디버깅을 하던 중, 생각과는 다르게 데이터가 필요한 시점마다 쿼리가 발생 하는 것으로 보이는 코드를 발견했고, 그것에 대해 고민했던 부분을 정리해보았다.


>> 상황

실행한 코드와 결과는 이렇다.

 

for user in User.objects.order_by('name')[:5]:
    print(user.stores.all())
2022-08-02 17:23:31,251 DEBUG django.db.backends debug_sql  (0.017) SELECT "users_user"."id", "users_user"."password", "users_user"."last_login", "users_user"."is_superuser", "users_user"."username", "users_user"."first_name", "users_user"."last_name", "users_user"."is_staff", "users_user"."is_active", "users_user"."date_joined", "users_user"."created", "users_user"."modified", "users_user"."remark", "users_user"."name", "users_user"."email", "users_user"."phone", "users_user"."mobile_carrier", "users_user"."gender", "users_user"."birthdate", "users_user"."profile_image", "users_user"."is_foreigner", "users_user"."di_code", "users_user"."ci_code", "users_user"."pg_provider", "users_user"."is_certified", "users_user"."recommender_code", "users_user"."is_guest" FROM "users_user" ORDER BY "users_user"."name" ASC LIMIT 5; args=()
2022-08-02 17:23:31,267 DEBUG django.db.backends debug_sql  (0.001) SELECT "stores_store"."id", "stores_store"."created", "stores_store"."modified", "stores_store"."remark", "stores_store"."user_id", "stores_store"."cfa_account_id", "stores_store"."business_registration_number", "stores_store"."title", "stores_store"."address", "stores_store"."representative_name", "stores_store"."is_active", "stores_store"."is_deleted", "stores_store"."price_policy_id", "stores_store"."manager_phone", "stores_store"."is_cardconnect_completed", "stores_store"."inflow_medium", "stores_store"."inflow_path", "stores_store"."business_type", "stores_store"."business_item" FROM "stores_store" WHERE (NOT "stores_store"."is_deleted" AND "stores_store"."user_id" = 369) LIMIT 21; args=(369,)
2022-08-02 17:23:31,275 DEBUG django.db.backends debug_sql  (0.000) SELECT "stores_store"."id", "stores_store"."created", "stores_store"."modified", "stores_store"."remark", "stores_store"."user_id", "stores_store"."cfa_account_id", "stores_store"."business_registration_number", "stores_store"."title", "stores_store"."address", "stores_store"."representative_name", "stores_store"."is_active", "stores_store"."is_deleted", "stores_store"."price_policy_id", "stores_store"."manager_phone", "stores_store"."is_cardconnect_completed", "stores_store"."inflow_medium", "stores_store"."inflow_path", "stores_store"."business_type", "stores_store"."business_item" FROM "stores_store" WHERE (NOT "stores_store"."is_deleted" AND "stores_store"."user_id" = 1064) LIMIT 21; args=(1064,)
2022-08-02 17:23:31,284 DEBUG django.db.backends debug_sql  (0.001) SELECT "stores_store"."id", "stores_store"."created", "stores_store"."modified", "stores_store"."remark", "stores_store"."user_id", "stores_store"."cfa_account_id", "stores_store"."business_registration_number", "stores_store"."title", "stores_store"."address", "stores_store"."representative_name", "stores_store"."is_active", "stores_store"."is_deleted", "stores_store"."price_policy_id", "stores_store"."manager_phone", "stores_store"."is_cardconnect_completed", "stores_store"."inflow_medium", "stores_store"."inflow_path", "stores_store"."business_type", "stores_store"."business_item" FROM "stores_store" WHERE (NOT "stores_store"."is_deleted" AND "stores_store"."user_id" = 1871) LIMIT 21; args=(1871,)
2022-08-02 17:23:31,294 DEBUG django.db.backends debug_sql  (0.002) SELECT "stores_store"."id", "stores_store"."created", "stores_store"."modified", "stores_store"."remark", "stores_store"."user_id", "stores_store"."cfa_account_id", "stores_store"."business_registration_number", "stores_store"."title", "stores_store"."address", "stores_store"."representative_name", "stores_store"."is_active", "stores_store"."is_deleted", "stores_store"."price_policy_id", "stores_store"."manager_phone", "stores_store"."is_cardconnect_completed", "stores_store"."inflow_medium", "stores_store"."inflow_path", "stores_store"."business_type", "stores_store"."business_item" FROM "stores_store" WHERE (NOT "stores_store"."is_deleted" AND "stores_store"."user_id" = 1144) LIMIT 21; args=(1144,)
2022-08-02 17:23:31,305 DEBUG django.db.backends debug_sql  (0.001) SELECT "stores_store"."id", "stores_store"."created", "stores_store"."modified", "stores_store"."remark", "stores_store"."user_id", "stores_store"."cfa_account_id", "stores_store"."business_registration_number", "stores_store"."title", "stores_store"."address", "stores_store"."representative_name", "stores_store"."is_active", "stores_store"."is_deleted", "stores_store"."price_policy_id", "stores_store"."manager_phone", "stores_store"."is_cardconnect_completed", "stores_store"."inflow_medium", "stores_store"."inflow_path", "stores_store"."business_type", "stores_store"."business_item" FROM "stores_store" WHERE (NOT "stores_store"."is_deleted" AND "stores_store"."user_id" = 1791) LIMIT 21; args=(1791,)

검색해보니, Django orm도 기본적으로 lazy loading을 지원하였고, 따라서 데이터가 사용되는 시점에 쿼리가 실행되는 1+N 현상이 일어나고 있었다.

이 코드에서 나는 다음과 같은 이유로 1+N이 발생하지 않는게 더 유리하다고 판단했다.

1. 해당 코드의 로직상 관련 객체의 미리 데이터를 불러오는 것이 유리하다고 판단했다.
2. 해당 객체의 정보를 가져올 때 따로 호출하는 내,외부 api가 없다.

>> 해결 과정

그렇다면 어떻게 해야 할까?에 대해 찾아보았는데, lazy loading을 eager loading으로 바꾸는 2가지 방법을 찾았고, 간략하게 정리하자면 다음과 같다.

# Prefetch_related 란?
1이 사용 할 수 있다. 파이썬 단에서 조인을 수행한다.

# Selected_related 란 ?
OneToOne/OneToMany에서 M이 사용할 수 있다.

eager loading 으로Data는 database 서버가 종료되기 전까지 Cache에 남아 있다.

https://docs.djangoproject.com/ko/5.0/ref/models/querysets/#select-related

> user : store = 1 : n 관계이므로 prefetch를 선택하였고 실행 결과는 다음과 같다.

for user in User.objects.prefetch_related('stores').order_by('name')[:5]: # eager loading
    print(user.stores.all())
2022-08-02 17:19:00,232 DEBUG django.db.backends debug_sql  (0.018) SELECT "users_user"."id", "users_user"."password", "users_user"."last_login", "users_user"."is_superuser", "users_user"."username", "users_user"."first_name", "users_user"."last_name", "users_user"."is_staff", "users_user"."is_active", "users_user"."date_joined", "users_user"."created", "users_user"."modified", "users_user"."remark", "users_user"."name", "users_user"."email", "users_user"."phone", "users_user"."mobile_carrier", "users_user"."gender", "users_user"."birthdate", "users_user"."profile_image", "users_user"."is_foreigner", "users_user"."di_code", "users_user"."ci_code", "users_user"."pg_provider", "users_user"."is_certified", "users_user"."recommender_code", "users_user"."is_guest" FROM "users_user" ORDER BY "users_user"."name" ASC LIMIT 5; args=()
2022-08-02 17:19:00,259 DEBUG django.db.backends debug_sql  (0.003) SELECT "stores_store"."id", "stores_store"."created", "stores_store"."modified", "stores_store"."remark", "stores_store"."user_id", "stores_store"."cfa_account_id", "stores_store"."business_registration_number", "stores_store"."title", "stores_store"."address", "stores_store"."representative_name", "stores_store"."is_active", "stores_store"."is_deleted", "stores_store"."price_policy_id", "stores_store"."manager_phone", "stores_store"."is_cardconnect_completed", "stores_store"."inflow_medium", "stores_store"."inflow_path", "stores_store"."business_type", "stores_store"."business_item" FROM "stores_store" WHERE (NOT "stores_store"."is_deleted" AND "stores_store"."user_id" 																																IN (369, 1064, 1871, 1144, 1791)); args=(369, 1064, 1871, 1144, 1791)

 


>> 결과

쿼리가 기존에는 조회하려는 n개의 객체의 정보에 대해 1+N 개의 쿼리가 발생하였다면, 코드 변경후에는 2개로 줄었고, 그에 따라 응답시간이 개선된 점을 확인할 수 있었다.


참고자료

https://docs.djangoproject.com/en/5.0/ref/models/querysets/
https://niceman.tistory.com/177
https://sebatyler.github.io/2016/05/31/django-performance.html
https://velog.io/@rosewwross/Django-selectrelated-%EC%99%80-prefetchedrelated%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B0%B8%EC%A1%B0)
https://inforyou.tistory.com/28
https://velog.io/@hoonki/Django-QuerySet-%EB%B6%84%EC%84%9D-%EB%B0%8F-%ED%8A%B9%EC%A7%95-%EC%B5%9C%EC%A0%81%ED%99%94