[django] select_related, prefetch_related 쿼리 실습

haejun-kim·2020년 8월 22일
1

[Django]

목록 보기
19/20

위코드 멘토 상록님께서 순수히 교육적인 목적으로 만드신 노션 페이지를 보고 실습 따라한 내용을 적은 포스팅

# select_related

정참조 관계에서 사용하며, JOIN 쿼리를 한번만 수행하고, category 정보는 cache되어 category name을 가져 올 때 db 쿼리를 하지 않는다. ( 데이터 베이스를 Hit 하지 않는다. )

>>> from product.models import *
>>> drink_all = Drink.objects.select_related('category').all()
>>> for drink in drink_all:
...     print(drink.category.name)
...
2020-08-22 16:47:54,942 DEBUG (0.001) SELECT "drinks"."id", "drinks"."name", "drinks"."menu_id", "drinks"."category_id", "drinks"."nutrition_id", "categories"."id", "categories"."name", "categories"."menu_id" FROM "drinks" LEFT OUTER JOIN "categories" ON ("drinks"."category_id" = "categories"."id"); args=()
콜드 브루 커피
콜드 브루 커피
콜드 브루 커피
콜드 브루 커피
콜드 브루 커피
브루드 커피
브루드 커피
에스프레소

출력 결과 확인 시 SQL LEFT OUTER JOIN이 사용 된 것을 확인 할 수 있다.

콜드 브루 커피 → drink.category.name을 가져오기 위하여 SELECT 와 같은 SQL 쿼리 하지 않았는데 이것이 database hit을 하지 않는다는 것을 의미한다.

실제로 menu는 select_related로 가져온 정보가 아니기 때문에 cache 되지 않아서 menu name을 출력할 때 마다 쿼리가 수행된다.

>>> for drink in drink_all:
...     print(drink.menu.name)
...
2020-08-22 16:50:25,875 DEBUG (0.000) SELECT "menus"."id", "menus"."name" FROM "menus" WHERE "menus"."id" = 1 LIMIT 21; args=(1,)
음료
2020-08-22 16:50:25,876 DEBUG (0.000) SELECT "menus"."id", "menus"."name" FROM "menus" WHERE "menus"."id" = 1 LIMIT 21; args=(1,)
음료
2020-08-22 16:50:25,876 DEBUG (0.000) SELECT "menus"."id", "menus"."name" FROM "menus" WHERE "menus"."id" = 1 LIMIT 21; args=(1,)
음료
2020-08-22 16:50:25,877 DEBUG (0.000) SELECT "menus"."id", "menus"."name" FROM "menus" WHERE "menus"."id" = 1 LIMIT 21; args=(1,)
음료
2020-08-22 16:50:25,877 DEBUG (0.000) SELECT "menus"."id", "menus"."name" FROM "menus" WHERE "menus"."id" = 1 LIMIT 21; args=(1,)
음료
2020-08-22 16:50:25,878 DEBUG (0.000) SELECT "menus"."id", "menus"."name" FROM "menus" WHERE "menus"."id" = 1 LIMIT 21; args=(1,)
음료
2020-08-22 16:50:25,879 DEBUG (0.000) SELECT "menus"."id", "menus"."name" FROM "menus" WHERE "menus"."id" = 1 LIMIT 21; args=(1,)
음료
2020-08-22 16:50:25,879 DEBUG (0.000) SELECT "menus"."id", "menus"."name" FROM "menus" WHERE "menus"."id" = 1 LIMIT 21; args=(1,)
음료

앞에서 출력 한 category name 에서는 SELECT로 SQL문이 실행 안되는 것과 비교했을 때, 이 경우는 공식 문서에 나와 있는 database를 hit 한다는 의미로 해석 할 수 있다.

prefetch

select_related와 달리 Many to One 또는 Many to Many 역참조 관계에서 별도의 2개의 쿼리를 수행 후에 파이썬에서 JOIN한다.

콜드 브루 커피인 카테고리 음료를 데이터베이스에서 쿼리

>>> prefetch_drink = Category.objects.prefetch_related('drink_set').get(id=1)
2020-08-22 16:54:43,756 DEBUG (0.000) SELECT "categories"."id", "categories"."name", "categories"."menu_id" FROM "categories" WHERE "categories"."id" = 1 LIMIT 21; args=(1,)
2020-08-22 16:54:43,760 DEBUG (0.003) SELECT "drinks"."id", "drinks"."name", "drinks"."menu_id", "drinks"."category_id", "drinks"."nutrition_id" FROM "drinks" WHERE "drinks"."category_id" IN (1); args=(1,)

출력 결과 확인 시 별도의 2개의 쿼리가 수행 되는 것을 확인할 수 있다.

>>> drink_data = [{'drink_name':drink.name}for drink in list(prefetch_drink.drink_set.all())]
>>> drink_data
[{'drink_name': '나이트로 바닐라 크림'}, {'drink_name': '제주 비자림 콜드 브루'}, {'drink_name': '코코넛 화이트 콜드 브루'}, {'drink_name': '나이트로 쇼콜라 클라우드'}, {'drink_name': '콜드 브루 몰트'}]

N:M 관계에서 알러지가 우유인 음료를 역참조 쿼리

>>> allergy_all = Allergy.objects.prefetch_related('allergydrink_set').get(id=1)
2020-08-22 16:56:55,617 DEBUG (0.001) SELECT "allergies"."id", "allergies"."name" FROM "allergies" WHERE "allergies"."id" = 1 LIMIT 21; args=(1,)
2020-08-22 16:56:55,619 DEBUG (0.001) SELECT "allergies_drinks"."id", "allergies_drinks"."allergy_id", "allergies_drinks"."drink_id" FROM "allergies_drinks" WHERE "allergies_drinks"."allergy_id" IN (1); args=(1,)
>>> allergy_data = [{'drinks':drink} for drink in list(allergy_all.allergydrink_set.all())]
>>> allergy_data
[{'drinks': <AllergyDrink: AllergyDrink object (1)>}, {'drinks': <AllergyDrink: AllergyDrink object (2)>}, {'drinks': <AllergyDrink: AllergyDrink object (4)>}]

TIP

  • many to many 관계에서 prefetch 사용시에 첫번째 / 마지막 결과 불러오는 방법, QuerySet으로 나오는 결과에 응용 가능

prefetch_drink = Category.objects.prefetch_related('drink_set')

prefetch_drink.first(), prefetch_drink.last()

prefetch_drink.first()
2020-04-21 02:58:27,885 DEBUG (0.000) SELECT "categories"."id", "categories"."name", "categories"."menu_id" FROM "categories" ORDER BY "categories"."id" ASC LIMIT 1; args=()
2020-04-21 02:58:27,887 DEBUG (0.000) SELECT "drinks"."id", "drinks"."name", "drinks"."menu_id", "drinks"."category_id", "drinks"."nutrition_id" FROM "drinks" WHERE "drinks"."category_id" IN (1); args=(1,)
<Category: Category object (1)>

객체 relation object 확인 방법

from django.db.models.fields.related import ForeignObjectRel

drink = Drink.objects.get(id=1)

links = [field.get_accessor_name() for field in drink._meta.get_fields() if issubclass(type(field), ForeignObjectRel)]

links
['image_set', 'description_set', 'allergydrink_set']

정리

select_related와 prefetch를 사용하는 이유는 결론적으로는 속도가 빠르기때문이며 이는 곧 성능이 좋다는 말과 연결된다.

여기서 나는 처음에는 database를 hit 하는 과정이 적기 때문에 속도가 빠르다고 생각했지만 실제로 database를 hit를 적게 하기 때문에 속도가 빠른것은 아니다. select_related 는 Query를 한번하고 캐쉬에 저장하고, prefetch는 Seperate 한 Query를 두 번 실행하지만 오히려 속도는 prefetch가 더 빠르다.

따라서 Hit 를 덜 하기 때문에 속도가 빠른것은 절대 아닌것에 주의하자.

  • select_related : 정참조 , one-to-one , one-to-many. prefetch보다 한번의 Query만 수행되기 때문에 리소스 소모를 줄일 수 있음
  • prefetch_related : 역참조 , 모든 관계에서 사용 가능. main query 외로 별도의 query를 수행. 한 번 이상의 Query를 수행. 리소스 소모 면에서는 selected_related가 유리. 하지만 속도면에서는 prefetch_related가 더 유리.

0개의 댓글