오늘도 개발

[2차 프로젝트 내일의 집] 포스트 목록 api - Django ORM 최적화하기 본문

TIL & 프로젝트 회고

[2차 프로젝트 내일의 집] 포스트 목록 api - Django ORM 최적화하기

Sueeeeeee 2022. 8. 7. 17:39

포스트 목록을 보내주는 api는 1차 때도 만들었지만,

2차 때는 Django ORM 최적화까지 해보기로 했다.

 

포스트 목록 api는 클라이언트가 요청을 보내면 '포스트 대표 이미지, 작성자, 포스트 속 첫번째 사진의 설명'을 보내주어야 한다.

포스트 대표 이미지는 posts 테이블에, 작성자는 users 테이블에, 포스트 속 첫번째 사진의 설명은 photos 테이블에 들어있다.

posts 기준으로 세 테이블의 정보를 가져오면서 매번 DB를 호출하지 않도록 코드를 짜는 것이 목적이다.

 

0. 기본 세팅

<models.py>

User-Post

한 유저는 여러 포스트를 작성할 수 있으므로 유저와 포스트는 일대다 관계이다.

Post가 User를 정참조한다. (필드 user)

 

Post-Photo

한 포스트에는 여러 사진이 들어갈 수 있으므로 포스트와 사진은 일대다 관계이다.

Post가 Photo를 역참조한다. (필드 post)

class Post(TimeStampModel):
    cover_image = models.URLField(max_length=300)
    user        = models.ForeignKey(User, on_delete=models.CASCADE)
    
class Photo(models.Model):
    description = models.TextField()
    url         = models.URLField(max_length=500)
    post        = models.ForeignKey(Post, on_delete=models.CASCADE)
    
class User(models.Model):
    kakao_id    = models.CharField(max_length=200, unique=True)
    nickname    = models.CharField(max_length=50, null=True)

<views.py>

10평 미만, 홈스타일링 값을 갖는 포스트만 보내준다고 가정한다.

아래 쿼리를 만족하는 posts는 총 3개 존재한다.

posts = Post.objects.filter(Q(room_size="10평 미만")&Q(work_type="홈스타일링"))

for post in posts:
    print(post.id)
    print(post.cover_image)
    
    # 일단은 모든 photo 오브젝트를 보여준다고 가정
    print(post.photo_set.all())
    
    print(post.user.id)
    print(post.user.nickname)

 

1. 최적화를 하지 않는 경우

총 7번 DB를 호출한다.

이유는 다음과 같다.

 

for문 시작 : 1) 실행 (posts 테이블 선택해서 캐싱)

첫번째 post 오브젝트 : 2) 실행(photos 테이블 선택), 3) 실행(users 테이블 선택)

두번째 post 오브젝트 : 2) 실행(photos 테이블 선택), 3) 실행(users 테이블 선택)

세번째 post 오브젝트 : 2) 실행(photos 테이블 선택), 3) 실행(users 테이블 선택)

posts = Post.objects.filter(Q(room_size="10평 미만")&Q(work_type="홈스타일링"))
# DB 호출 없음, 1번 SQL 쿼리 준비

for post in posts:
# 1) DB 호출 후 1번 SQL 쿼리 실행
# posts 테이블 캐싱
    print(post.id)
    print(post.cover_image)
    print(post.photo_set.all()[0].description)
    # 2) DB 호출 후 2번 SQL 쿼리 생성해서 실행
    # photos 테이블 캐싱
    print(post.user.id)
    # 3) DB 호출 후 3번 SQL 쿼리 생성해서 실행
    # user 테이블 캐싱
    print(post.user.nickname)

1) 1번 SQL 쿼리(posts 테이블 선택해서 캐싱)

SELECT `posts`.`id`, `posts`.`created_at`, `posts`.`updated_at`, `posts`.`title`, `posts`.`content`, `posts`.`cover_image`, `posts`.`living_type`, `posts`.`room_size`, `posts`.`family_type`, `posts`.`work_type`, `posts`.`worker_type`, `posts`.`user_id` 
FROM `posts` 
WHERE (`posts`.`room_size` = '10평 미만' AND `posts`.`work_type` = '홈스타일링');

2) 2번 SQL 쿼리(users 테이블 선택해서 캐싱)

SELECT `photos`.`id`, `photos`.`description`, `photos`.`url`, `photos`.`post_id` 
FROM `photos` 
WHERE `photos`.`post_id` = 1 ORDER BY `photos`.`id`;

3) 3번 SQL 쿼리(photos 테이블 선택해서 캐싱)

SELECT `users`.`id`, `users`.`created_at`, `users`.`updated_at`, `users`.`kakao_id`, `users`.`email`, `users`.`nickname`, `users`.`profile_image` 
FROM `users` 
WHERE `users`.`id` = 1;

 

2. select_related()만 사용하는 경우

posts 테이블은 users 테이블을 정참조하므로 select_related()를 사용할 수 있다.

select_related()를 사용했을 때 총 4번 DB를 호출한다.

 

for문 시작 : 1) 실행 (posts 테이블과 users 테이블 조인해서 캐싱)

첫번째 post 오브젝트 : 2) 실행(photos 테이블 쿼리)

두번째 post 오브젝트 : 2) 실행(photos 테이블 쿼리)

세번째 post 오브젝트 : 2) 실행(photos 테이블 쿼리)

posts = Post.objects.filter(Q(room_size="10평 미만")&Q(work_type="홈스타일링")).select_related('user')
# DB 호출 없음, 1번 SQL 쿼리 준비

for post in posts:
# 1) DB 호출 후 1번 SQL 쿼리 실행
# posts와 users를 조인한 테이블 캐싱
    print(post.id)
    print(post.cover_image)
    print(post.photo_set.all()[0].description)
    # 2) DB 호출 후 2번 SQL 쿼리 생성해서 실행
    # photos 테이블 캐싱
    print(post.user.id)
    print(post.user.nickname)

1) 1번 SQL 쿼리(posts 테이블과 users 테이블을 join해서 가져온 후 캐싱)

SELECT `posts`.`id`, `posts`.`created_at`, `posts`.`updated_at`, `posts`.`title`, `posts`.`content`, `posts`.`cover_image`, `posts`.`living_type`, `posts`.`room_size`, `posts`.`family_type`, `posts`.`work_type`, `posts`.`worker_type`, `posts`.`user_id`, `users`.`id`, `users`.`created_at`, `users`.`updated_at`, `users`.`kakao_id`, `users`.`email`, `users`.`nickname`, `users`.`profile_image` 
FROM `posts` INNER JOIN `users` 
ON (`posts`.`user_id` = `users`.`id`) 
WHERE (`posts`.`room_size` = '10평 미만' AND `posts`.`work_type` = '홈스타일링');

2) 2번 SQL 쿼리(photos 테이블 선택해서 캐싱)

SELECT `photos`.`id`, `photos`.`description`, `photos`.`url`, `photos`.`post_id` 
FROM `photos` WHERE `photos`.`post_id` = 1 
ORDER BY `photos`.`id` ASC LIMIT 1;

 

3. prefetch_related()도 사용하는 경우

posts 테이블은 photos 테이블을 역참조하므로 prefetch_related()를 사용할 수 있다.

select_related()와 prefetch_related()를 같이 사용하면 총 2번만 DB를 호출한다.

 

for문 시작 : 1) 실행(posts 테이블과 users 테이블 조인해서 캐싱), 2) 실행(photos 테이블 캐싱)

첫번째 post 오브젝트 : posts, users, phots 모두 캐싱된 데이터를 가져옴

두번째 post 오브젝트 : posts, users, phots 모두 캐싱된 데이터를 가져옴

세번째 post 오브젝트 : posts, users, phots 모두 캐싱된 데이터를 가져옴

posts = Post.objects.filter(Q(room_size="10평 미만")&Q(work_type="홈스타일링")).select_related('user').prefetch_related('photo_set')
# DB 호출 없음, 1번과 2번 SQL 쿼리 준비

for post in posts:
# 1) DB 호출 후 1번 SQL 쿼리 실행 => posts와 users를 조인한 테이블 캐싱
# 2) DB 호출 후 1번 SQL 쿼리 실행 => photos 테이블 캐싱
    print(post.id)
    print(post.cover_image)
    print(post.photo_set.all()[0].description)
    print(post.user.id)
    print(post.user.nickname)

1) 1번 SQL 쿼리(posts 테이블과 users 테이블을 join해서 가져온 후 캐싱)

SELECT `posts`.`id`, `posts`.`created_at`, `posts`.`updated_at`, `posts`.`title`, `posts`.`content`, `posts`.`cover_image`, `posts`.`living_type`, `posts`.`room_size`, `posts`.`family_type`, `posts`.`work_type`, `posts`.`worker_type`, `posts`.`user_id`, `users`.`id`, `users`.`created_at`, `users`.`updated_at`, `users`.`kakao_id`, `users`.`email`, `users`.`nickname`, `users`.`profile_image` 
FROM `posts` INNER JOIN `users` 
ON (`posts`.`user_id` = `users`.`id`) 
WHERE (`posts`.`room_size` = '10평 미만' AND `posts`.`work_type` = '홈스타일링');

2) 2번 SQL 쿼리(photos 테이블 캐싱)

SELECT `photos`.`id`, `photos`.`description`, `photos`.`url`, `photos`.`post_id` 
FROM `photos` 
WHERE `photos`.`post_id` IN (1, 11, 21);