[Django] Lazy-Loading

Bik_Kyun·2022년 3월 20일
0
post-thumbnail

1. ORM에서의 Lazy-Loading

ORM에선 DB의 리소스를 정말 필요한 때가 아니면 호출하지 않는다.

users = User.objects.all() # DB에서 호출 X
user_list = list(users)    # DB에서 호출 O

인터프리터에서 처리하는 라인에서 해당 값이 사용될 때 까지 쿼리문은 수행되지 않는다.
성능차원에서 DB엑세스를 최소화하기 위해 사용되나 비효율적인 쿼리가 발생하므로 ...

2. Lazy-Loading의 문제점

들이 발생한다.

1) 필요한 시점에 SQL문을 호출한다 -> 여러개의 쿼리셋이 한번에 실행되면 매우 느리게 동작할 수 있다.

여러 개의 쿼리가 합쳐져 Join(Left Outer Join)이 많아지고 복잡해지면 성능이 저하된다.

  • 데이터구조를 개선하는 방식으로 이를 해결하려면 정규화 레벨을 낮추거나 DB를 NoSQL 방식으로 변경하는 방법이 있다.
  • 어플리케이션 조인 방식으로 이를 해결하려면 어플리케이션에서 각각의 테이블 컬렉션을 가져온 후 테이블 간 관계되는 key값을 통해 따로 처리하는 방식이 있다.
    ⚠️ 이 방식의 경우 Where OO in 1,2,3...의 쿼리로 추가하여 처리를 하는데 이 때 인덱스는 200개를 넘지않도록 하는게 통상적이고 200개가 넘어가는 경우 별도의 쿼리문을 통해 다시 가져오도록 파티셔닝해야(인덱스 파트 별로 분산해야 한다는 뜻) 성능상으로 이득을 볼 수 있다.

2) 정말 필요한 만큼 호출한다 -> 이미 알고 있는 값도 다시 호출될 수 있다.(N+1 Query Problem)

N+1 문제는 연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우(외래키를 통해 테이블 참조) 조회된 데이터 갯수(N) 만큼 연관관계의 조회 쿼리가 추가로 발생되는 것.

  • 예시
class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

    def __str__(self):
        return self.name


class Restaurant(models.Model):
    place = models.OneToOneField(Place, on_delete=models.CASCADE, related_name='restaurant')
    name = models.CharField(max_length=50)
    severs_pizza = models.BooleanField(default=False)

    def __str__(self):
        return self.name
        
if __name__ == '__main__':
  for place in Place.objects.all():
     print(place.restaurant.name)
  • 수행된 쿼리
SELECT `photo_place`.`id`, `photo_place`.`name`, `photo_place`.`address` FROM `photo_place`
SELECT `photo_restaurant`.`id`, `photo_restaurant`.`place_id`, `photo_restaurant`.`name`, `photo_restaurant`.`severs_pizza` FROM `photo_restaurant` WHERE `photo_restaurant`.`place_id` = 1 LIMIT 21
SELECT `photo_restaurant`.`id`, `photo_restaurant`.`place_id`, `photo_restaurant`.`name`, `photo_restaurant`.`severs_pizza` FROM `photo_restaurant` WHERE `photo_restaurant`.`place_id` = 2 LIMIT 21
SELECT `photo_restaurant`.`id`, `photo_restaurant`.`place_id`, `photo_restaurant`.`name`, `photo_restaurant`.`severs_pizza` FROM `photo_restaurant` WHERE `photo_restaurant`.`place_id` = 3 LIMIT 21
SELECT `photo_restaurant`.`id`, `photo_restaurant`.`place_id`, `photo_restaurant`.`name`, `photo_restaurant`.`severs_pizza` FROM `photo_restaurant` WHERE `photo_restaurant`.`place_id` = 4 LIMIT 21

3. Eager-Loading

쿼리를 하나씩 실행하여 데이터를 가져오는 Lazy-Loading 방식과는 달리,
Eager-Loading은 추후에 사용할 데이터를 포함해 쿼리를 날리기 때문에 비효율적으로 쿼리가 늘어나는 것을 방지할 수 있다.
위에서 서술한 N+1 Query Problem을 해결하는 방식으로 많이 사용한다.

참조 대상이 중간 테이블이 아닐 때 쿼리문에서 Join을 이용해 쿼리를 수행하는 메소드.
1) 사용하려는 객체가 정참조(다른 객체의 Foreign Key를 가지고 있는 경우)인 경우
2) Foreign Key가 없어도 1:1 관계에 있는 객체의 데이터를 캐싱해 오는 경우

  • 예시
dogs = Dog.objects.select_related('owner').filter(id__gt=65540)
for dog in dogs:
	dog.owner.name
  • 수행된 쿼리
SELECT `dogs`.`id`, `dogs`.`owner_id`, `dogs`.`name`, `dogs`.`age`, `owners`.`id`, `owners`.`name`, `owners`.`email`, `owners`.`age`
FROM `dogs`
INNER JOIN `owners` ON (`dogs`.`owner_id` = `owners`.`id`)
WHERE `dogs`.`id` > 65540; args=(65540,)

해당 객체가 역참조될 경우에 해당 객체들의 데이터를 캐싱해오는 메소드.
2개의 테이블을 각각 읽어와서 Django에서 합친다.
2번째 테이블을 읽어올 때, IN을 사용하여 필요한 만큼의 쿼리만 수행한다.
select_related()를 사용할 수 없는 N:N 관계에서 사용되나 1:1, 1:N 관계에서도 사용가능하다.

  • 예시
dogs = Dog.objects.prefetch_related('owner').filter(id__gt=65540)
for dog in dogs2:
	dog.owner.name
  • 수행된 쿼리
SELECT `dogs`.`id`, `dogs`.`owner_id`, `dogs`.`name`, `dogs`.`age` FROM `dogs` WHERE `dogs`.`id` > 65540; args=(65540,)
SELECT `owners`.`id`, `owners`.`name`, `owners`.`email`, `owners`.`age` FROM `owners` WHERE `owners`.`id` IN (17); args=(17,)
profile
비진

0개의 댓글