Django QuerySet 활용(select_related & prefetch_related)

seyong·2021년 12월 26일
0
post-thumbnail

select_related, prefetch_related 쿼리셋은 바로 SQL의 join 기능으로 활용할 수 있는 쿼리셋이다.

참고로 먼저 간단히 설명해보자면,

select_related 는 정참조 관계일때, 사용할수 있으며
prefetch_related 는 역참조 관계일때, 사용할수 있다.

데이터베이스에 쿼리를 최대한 적게 날려서
DB hit를 줄여서 DB에 부하가 최대한 덜 가도록 하는,
ORM 최적화에 자주 쓰이는 쿼리셋이다.

스타벅스 음료홈페이지를 참고하여 models.py를 작성한 것을 예를 들어 설명해보겠다.


from django.db import models

class Menu(models.Model):
	name = models.CharField(max_length=45)
 
    class Meta:
        db_table = 'menus'
 
 
class Category(models.Model):
    	menu         =   models.ForeignKey(Menu,on_delete=models.CASCADE)
    	name         =   models.CharField(max_length=45)

    class Meta:
        db_table = 'categories'


 class Drink(models.Model):
    	category        =   models.ForeignKey(Category,on_delete=models.SET_NULL,null=True)
    	name            =   models.CharField(max_length=45)
    	nutrition       =   models.OneToOneField('Nutrition', on_delete = models.CASCADE)
    	allergy_drink   =   models.ManyToManyField('Allergy', through='AllergyDrink')

     class Meta:
        db_table = 'drinks'
        

여기서 Menu는 음료, 베이커리, 텀블러와 같은 대분류이다. 그리고 Category는 Menu의 id를 참조해서 생성된다. 음료에 콜드브루, 주스 등이 있는 형태를 의미한다.

Drink는 category의 id를 참조한 음료 하나를 의미한다. 콜드브루, 아메리카노, 카페라떼 등등..

이제 select_related 을 활용하기 위해 한 가지 가정을 해보겠다.
음료에서 각 음료가 어떤 카테고리에 속하는지 알고싶은 상황으로 가정해보겠다.

기존의 경우에는 각 Drink(음료)의 category_id를 찾고, 그리 category_id의 name으로 확인을 했다. 아래 코드에서 기존 방법을 확인해보자. (select_related 를 사용하지 않았을때)


>>> drink_all = Drink.objects.all()
>>> print(drink_all.query)
SELECT `drinks`.`id`, `drinks`.`category_id`, `drinks`.`name`, `drinks`.`nutrition_id` FROM `drinks`
>>> for drink in drink_all:
...     print(Category.objects.get(id=drink.category_id).name)
콜드 브루
콜드 브루
콜드 브루
콜드 브루
콜드 브루
......(생략)

먼저 Drink class의 모든 객체를 받는다. 이 쿼리셋의 쿼리를 확인하려면 .query를 붙여서 확인할 수 있다.
다만, 이미 객체나 변수로 받은 경우에는 확인할 수 없다.

drinks 테이블 전체를 select 하고 나면 그 대상에 category_id만으로 join과정을 거친다. 그리고 name 값을 받아 온다. 쿼리문으로 표현한다면 아래와 같다.


SELECT `drinks`.`id`, `drinks`.`category_id`, `drinks`.`name`, `drinks`.`nutrition_id` FROM `drinks`
SELECT category.name from categories WHERE category_id=drinks.category_id

위와 같이 두 번의 쿼리가 실행된다. 이 경우라면 데이터베이스에 Connection을 두 번 맺어 쿼리를 수행하는 형태가된다.
위와 같은 경우는 당연히 시간적으로 더 소요될 수 밖에 없을 것이다.

그렇다면 select_related를 사용하면 위와 비교하여 어떻게 실행될까?
아래를 보면 알 수 있다.


>>> drink_all=Drink.objects.select_related('category').all()
>>> for drink in drink_all:
...     print(drink.category.name)
...
콜드 브루
콜드 브루
콜드 브루
....
>>> print(drink_all.query)
SELECT `drinks`.`id`, `drinks`.`category_id`, `drinks`.`name`, `drinks`.`nutrition_id`, `categories`.`id`, `categories`.`menu_id`, `categories`.`name` FROM `drinks` LEFT OUTER JOIN `categories` ON (`drinks`.`category_id` = `categories`.`id`)

코드블럭 내용중에서 아래의 쿼리문을 보면
select_related 에서 쿼리가 하나로 나오는 것을 볼 수 있다.
왜냐하면 LEFT OUTER JOIN으로 가져 올 수 있기 때문이다.
즉, DB의 hit를 한 번만 하고도 결과 값을 가져올 수 있는것이다.

여기서 참조할 부분이있다.
위에서 설정한 models.py 에서 Drink class에서 ForeignKey(Category 객체) 삭제 시, Null로 변환하는 설정을 넣었기 때문에 JOIN이 안 되는 것을 방지하고자, LEFT OUTER JOIN으로 자동 수행된다.
만약 CASCADE 설정을 걸었으면, INNER JOIN으로 수행된다.

select_relatedManyToOne관계 또는 ManyToMany관계 에서 정참조관계일때 JOIN에 유리하다.
(여기서는 Drink에서 Category관련 정보를 찾고자 할 때)

위에서는 select_related를 통한 JOIN을 확인했다. 확인하여보니 select_related 는 DB에 한 번만 접속해서 가져오는 방식이였다.

반면, prefetch_related 는 어떤지 알아보자.
위와 마찬가지로 상황을 가정해보겠다.
스타벅스 음료 카테고리 중 '브루드 커피' 인 메뉴를 전부 확인하고자 한다.

먼저 기존의 방식대로 Category 이름으로 해당 id를 찾고, 그 brewed coffee에 해당하는 id로 filter를 거쳐 brewed coffee의 drink id를 받아온다. 그리고 쿼리셋에서 각 커피의 이름을 출력하면 확인할 수 있다.


>>> brewed_coffee_id = Category.objects.get(name="브루드 커피")
>>> brewed_coffe_list = Drink.objects.filter(category_id=brewed_coffee_id)
>>> for coffee in brewed_coffe_list:
...     print(coffee.name)
...
아이스 커피
오늘의 커피

SELECT id FROM categories where name="브루드 커피"
>>> print(brewed_coffe_list.query)
SELECT `drinks`.`id`, `drinks`.`category_id`, `drinks`.`name`, `drinks`.`nutrition_id` FROM `drinks` WHERE `drinks`.`category_id` = 2

SQL 쿼리로 나타내면 위와 같이 크게 2단계로 볼 수 있다. "브루드"에 해당하는 id를 구하고 그 커피를 select 한다.

마찬가지로 두 번의 쿼리가 실행되는 것을 확인할 수 있다.
비교를위해서 이번에는 prefetch_related 로 확인해보겠다.
아래를 보자.


>>> prefetch_drink = Category.objects.prefetch_related('drink_set').get(name="브루드 커피")
>>> drink_data = [ {'drink_name' : drink.name } for drink in list(prefetch_drink.drink_set.all())]
>>> drink_data
[{'drink_name': '아이스 커피'}, {'drink_name': '오늘의 커피'}]

SELECT "categories"."id", "categories"."name", "categories"."menu_id" FROM "categories" WHERE "categories"."id" = 2
SELECT "drinks"."id", "drinks"."name", "drinks"."menu_id", "drinks"."category_id", "drinks"."nutrition_id" FROM "drinks" WHERE "drinks"."category_id"

위와 같이 prefetch_related는 수행 시 두 번의 쿼리를 수행해서 python에서 JOIN을 한다.

select_related 와는 반대로 역참조 관계(카테고리를 통해 음료의 정보를 참조할 때)에서 유용하게 사용된다.

select_related 는 정참조 관계일때 사용하고 쿼리를 한번만 날리고,
prefetch_related는 역참조 관계일때 사용하고 쿼리를 두번 날린다.

간단하게 위 2가지에 대해서 알아보았지만 아직은 사용하는데 익숙치 않고
어려운 느낌이어서 쉘에서 자주 쳐보면서 공부해야겠다.

profile
# 불편함을 편리함으로 바꾸고싶은 주니어 Back-end 개발자

0개의 댓글