TIL - select_related와 prefetch_related

Heechul Yoon·2020년 2월 18일
1

LOG

목록 보기
11/62

스타벅스 상품페이지 django 모델링을 살펴보자

class Size(models.Model):
    sizeName  = models.CharField(max_length = 50, null=True)
    sizeGauge = models.DecimalField(max_digits=5, decimal_places=2, null=True)
    class Meta:
        db_table = 'sizes'

class Nutrition(models.Model):
    totalAmount = models.DecimalField(max_digits=5, decimal_places=2, null=True)
    fat         = models.DecimalField(max_digits=5, decimal_places=2, null=True)
    protein     = models.DecimalField(max_digits=5, decimal_places=2, null=True)
    natryum     = models.DecimalField(max_digits=5, decimal_places=2, null=True)
    suger       = models.DecimalField(max_digits=5, decimal_places=2, null=True)
    caffeine    = models.DecimalField(max_digits=5, decimal_places=2, null=True)
    class Meta:
        db_table = 'nutritions'
#테이블명이 있기 때문에 컬럼들은 too much detail할 필요없다.
#컬럼 명  줄때 장고가 포린키 주면 장고에서 알아서 _id해준다.
#컬럼 명 줄때 _포함해도 됨.

class DrinkCategories(models.Model):
    name = models.CharField(max_length = 50, null=True)
    class Meta:
        db_table = 'drinkCategories'
#만약 subcategory말고 drinkcategory로 만들어주면 카테코리 만들때마다 테이블만들어야함. subcategory로
class MainCategory(models.Model):
    drink_category        = models.ForeignKey('DrinkCategories', models.SET_NULL,null=True)
    class Meta:
        db_table = 'mainCategories'

class Allergy(models.Model):
    allergyType = models.CharField(max_length = 50, null=True)
    class Meta:
        db_table = 'allergies'

class Item(models.Model):
    name               = models.CharField(max_length = 50, null=True)
    nameEnglish        = models.CharField(max_length = 50, null=True)
    description        = models.CharField(max_length = 50, null=True)
    thumbnail          = models.CharField(max_length = 200, null=True)
    drink_category     = models.ForeignKey('DrinkCategories', on_delete = models.SET_NULL, null = True)
    nutrition          = models.ForeignKey('Nutrition', on_delete = models.SET_NULL, null = True)
    size               = models.ForeignKey('Size', on_delete = models.SET_NULL, null = True)
    allergy            = models.ManyToManyField('Allergy', through='ItemAllergy')
    class Meta:
        db_table = 'items'

class ItemAllergy(models.Model):
    item    = models.ForeignKey('Item', on_delete = models.SET_NULL, null = True)
    allergy = models.ForeignKey('Allergy', on_delete = models.SET_NULL, null = True)
    class Meta:
        db_table = 'itemAllergies'

특징은 Item테이블이 대부분의 테이블을 정참조하고, allergy테이블의 경우 ManyToMany관계를 형성하고 있다.

select_related

select_related를 사용해서 ORM의 쿼리를 줄여보자.
ORM과 데이터베이스간의 소통에서 쿼리가 발생해 값을 주고받는 과정은 웹서비스 차원에서 많은 트레픽을 가져온다.

select_related는 언제 사용할까

  • OneToOne, ForeignKey참조에서 정참조의 경우
Item.objects.all()[3].size.sizeName

위의 코드는 객체를 쿼리셋으로 전부 들고와서 인덱싱해주고 객체안에 size_id(Item필드 안의 size_id 컬럼)에 해당하는 size테이블로 이동 후 sizeName값을 가져온다.

적어도 두번이상(처음에 all로불러올때, size테이블로갈때)의 쿼리가발생한다.

select_related로 쿼리줄이기

Item.objects.select_related('size')[3].size.sizeName - 보기에는 더 길어져 보이지만 select_related라는 매서드가 size_id필드(Size테이블을 참조하는)가 참조하는 Size테이블을 caching하고 size 테이블로 access해서 sizeName을 가져온다.

즉, foriegnKey를 가지고있는 컬럼의 테이블까지 caching을 해옴으로서 query를 줄여주는 효과를 가져온다.

ManyToMany도 관점에 따라 select_related를 사용할 수 있다.

ManyToMany관계에서 중간관리자 테이블의 입장에서 보면 양측 테이블 모두 정참조이다. 이경우에 데이터의 러올 때 중간 테이블의 관점에서 select_related를 사용할 수 있다.

(1)Item.objects.select_related('item').filter(item_id=1)
(2)Item.objects.filter(item_id=1)

(1)코드와 (2)코드의 차이는 무엇일까?
(2)의 코드가 단순히 item_id=1과인 객체를 전부 불러오는 매서드라면, (1)의 코드는 item필드가 Item테이블과 연관되어 있기 때문에 이 객체중(이과정에서 Item테이블을 caching으로 저장한다) item_id=1인 객체를 불러오는것이다.

(1)과(2)는 이단계까지는 한번의 쿼리스트링으로 그친다. 하지만 좀 더 파고들어 실재로 Item테이블의 객체를 가져와보자.

(1)Item.objects.select_related('Item').filter(item_id=1)[0].item.name
(2)Item.objects.filter(item_id=1)[0].item.name

(1)과(2)는 마찬가지로 item테이블에 접근해서 name value를 가져온다. 하지만 1번의 경우 select_related로 caching된 item table을 가져오기 때문에 쿼리스트링이 발생하지않는다.

prefetch_related

ManyToMany관계인경우

ManyToMany관계는 둘의 관계를 중간관리 테이블에서 관리한다. 알러지를 유발하는 제료를 예로들어보면 우유, 대두, 견과류 등이 있다. 상품의 종류도 아이스라떼, 프라푸치노, 카페모카 등 이 있다. 여기서 우유는 아이스라떼에도 카페모카에도 들어간다. 그리고 프라푸치노또한 우유, 대두, 견과류를 포함할 수 있다. 즉, 하나의 객체가 여러가지 대상을 가질 수 있다는 뜻이다. 이를 ManyToMany관계라고 한다.

Item.objects.prefetch_related('allergy').filter(name='아이스라떼')

코드를 보자. 위의 코드는 allergy테이블을 caching한다는 뜻이다. 즉, allergy테이블을 일단 캐시데이터로 가져와 저장한다. 그리고 제품명이 아이스라떼인 객체를 쿼리셋에 담아서 가져온다.

<QuerySet [<Item: Item object (1)>]>
하나의 객체가 딸려왔다.

이제 allergy테이블로 접근해보자.

Item.objects.prefetch_related('allergy').filter(name='아이스라떼')[0].allergy

하나의 객체를 인덱싱해주고 select_related에서 했던 것 처럼 .allergy를 통해서 allergy테이블로 접근한다.(여기서 allergy는 Item테이블안에 있는 allergy_id이다!)

<django.db.models.fields.related_descriptors.create_forward_many_to_many_manager..ManyRelatedManager at 0x7fdd4a141dd8>
select_related의 경우에는 조건에 해당하는 allergy객체를 리턴했지만 원하지않는 값이 리턴되었다.

In [138]: Item.objects.prefetch_related('allergy').filter(name='아이스라떼')[0].allergy.values()            
Out[138]: <QuerySet [{'id': 1, 'allergyType': '대두'}, {'id': 2, 'allergyType': '우유'}]>

values를 값을 불러와보면 해당 filter에 맞는 값이 쿼리셋으로 value형태로 리턴되었다. 즉, django ORM이 알아서 '아이스라떼'에 해당하는 id를 ItemAllergy중간테이블과 Allergy테이블을 비교해서 매칭되는 값을 리턴한 것이다.

In [140]: Item.objects.prefetch_related('allergy').filter(name='아이스라떼')[0].allergy.all()               
Out[140]: <QuerySet [<Allergy: Allergy object (1)>, <Allergy: Allergy object (2)>]>

all()을 통해서 값을 불러오면 쿼리셋으로 객체자체를 가져온다. 즉, ManyToMany의 관계이기 때문에 한개이상의 값을 가정하고 쿼리를 보내야 한다는 것이다.

역참조(reverse_related)의 경우

  1. 테이블을 모델링 할 때 설정
class Item(models.Model):
    name               = models.CharField(max_length = 50, null=True)
    nameEnglish        = models.CharField(max_length = 50, null=True)
    description        = models.CharField(max_length = 50, null=True)
    thumbnail          = models.CharField(max_length = 200, null=True)
    drink_category     = models.ForeignKey('DrinkCategories', on_delete = models.SET_NULL, null = True)
    nutrition          = models.ForeignKey('Nutrition', on_delete = models.SET_NULL, null = True)
    size               = models.ForeignKey('Size', on_delete = models.SET_NULL, null = True)
    allergy            = models.ManyToManyField('Allergy', through='ItemAllergy')
    class Meta:
        db_table = 'items'

class Size(models.Model):
    sizeName  = models.CharField(max_length = 50, null=True)
    sizeGauge = models.DecimalField(max_digits=5, decimal_places=2, null=True)
    class Meta:
        db_table = 'sizes'

Item테이블은 Size테이블을 정참조하고, Size테이블은 Item테이블을 역참조한다. 그렇다면 Size테이블의 관점에서 참조당하는 객체를 가져오려면 어떻게해야할까?

class Item(models.Model):
      size = models.ForeignKey('Size', related_name='items' on_delete = models.SET_NULL, null = True)

위와같이 테이블 모델링부터 참조하는(size테이블을 보고있는)테이블이 자기가 참조당하는 입장이되었을 때 related_name='items'와 같이 이름을 붙혀준다.

  1. 쿼리스트링을 보낼 때

In [190]: Size.objects.prefetch_related('item_set')
Out[190]: <QuerySet [<Size: Size object (1)>, <Size: Size object (2)>, <Size: Size object (3)>, <Size: Size object (4)>]>

위와같이 기존에 prefetch_related('item')만 lowercase로 적어줬던 것과 달리 'item'뒤에 '_set'을 붙혀주면 Size테이블입장에서 자신을 참조하는 테이블의 객체를 쿼리셋으로 가져온다. 그렇다면 여기서 Item테이블로 어떻게 갈까??(여기서 item_set의 item은 Item테이블이다.)

In [203]: Size.objects.prefetch_related('item_set')[3].item_set
Out[203]: <django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager..RelatedManager at 0x7fdd4a08aac8>

쿼리셋에서 3번값을 인덱싱해주고 .item_set을 사용해서 Item테이블로 넘어간다. select_related와 ManyToMany관계에서의 prefetch_related와 다르게 역참조의 경우 'item'뒤에 마찬가지로 '_set'을 붙혀준다.

위에서 output으로 객체가 나오지 않았지만 이미 .item_sit을 사용해줌으로 인해서 Item테이블로 넘어간 상태이기 때문에, .values() 또는 .all() 또는 filter()을 사용해서 원하는 값들을 가져올 수 있다.

In [206]: Size.objects.prefetch_related('item_set')[2].item_set.values()
Out[206]: <QuerySet [{'id': 4, 'name': '프라푸치노', 'nameEnglish': 'frafchino', 'description': '맛있음', 'thumbnail': 'url@url.com', 'drink_category_id': 1, 'nutrition_id': 1, 'size_id': 3}]>

values()를 다들고 왔는데 Item 중에서 id가 4인 하나의 객체만 가져왔다. 그 이유는 오직 이 객체만이 Size테이블을 정참조 하고 있기 때문이다.(Size테이블 입장에서는 역참조)

즉, size테이블에서 역참조로 타고 온거니깐, Item테이블에 size_id값이 Null이 아닌 경우에만 값을가져온다.

그렇다면 Size테이블의 id가3인 객체의 사이즈이름과 그 객체를 보고있는 Item테이블의 객체의 이름을 알고싶다면 어떻게 해야할까. 즉, 해당테이블의 객체에 있는 value와 그객체를 역참조하고있는 테이블의 객체의 이름을 알고싶다면??

lookup key를 사용하면 된다.

In [240]: Size.objects.filter(id=3).prefetch_related('item_set').values('id', 'sizeName', 'item__name')     
Out[240]: <QuerySet [{'id': 3, 'sizeName': 'benti', 'item__name': '프라푸치노'}]>

위의와같이 Size테이블에서 id=3인 필터를 걸어주고, prefetch_related로 item테이블에있는 객체를 caching한다.(caching했다고 해서 size테이블에서 item테이블로 이동했다는 것이아니다)그리고 Size테이블의 id, sizeName을 불러오고 itemname으로 Item테이블의 name 값을 불러온다. 'name'이라는 lookup 규칙(?)을 사용해서 Size테이블의 객체와함께 Item테이블의 객체값을 불러온 것이다.

prefetch_related를 썼을 때와 안썼을 때.

In [255]: Size.objects.filter(id=3).prefetch_related('item_set')[0].item_set.values()                       
Out[255]: <QuerySet [{'id': 4, 'name': '프라푸치노', 'nameEnglish': 'frafchino', 'description': '맛있음', 'thumbnail': 'url@url.com', 'drink_category_id': 1, 'nutrition_id': 1, 'size_id': 3}]>

In [256]: Size.objects.filter(id=3)[0].item_set.values()                                                    
Out[256]: <QuerySet [{'id': 4, 'name': '프라푸치노', 'nameEnglish': 'frafchino', 'description': '맛있음', 'thumbnail': 'url@url.com', 'drink_category_id': 1, 'nutrition_id': 1, 'size_id': 3}]>

위의 두가지경우는 '_set'을 통해서 역참조를 한 경우이다. 두가지 경우 다 id=3인 Size테이블의 객체를 바라보고있는 Item테이블의 객체의 values를 가져왔다.

그렇다면 prefetch_related는 왜 쓰는 걸까?

답은 쿼리를 줄이는데 있다. 앞서설명했듯이 prefetch_related와 select_related를 사용하면 참조관계에 있는 테이블을 캐싱한다고 했다.

prefetch_related를 사용하지않은 두번째 경우는 같은결과값을 가져왔지만 item_set 매서드로 Item 테이블로 acess했을 때 Item테이블을 캐싱하지 않았기 때문에 쿼리를 한번 더 추가한다.

prefetch_related를 사용해서 코드는 길어졌지만 쿼리의 효율성을 위해서 경우에 따라 추가해주는것이 좋을 것 같다

profile
Quit talking, Begin doing

0개의 댓글