이번 포스팅에서는 다양한 예시 ORM을 통해 실제로 생성되는 SQL에 대해 알아볼 것이다. 지난 포스팅에 이어 select_related(), prefetch_related() 를 중점으로 다룰 것이다. 또한, ORM이 생성하는 SQL 구조는 어떻게 되는지, 추천하는 ORM 옵션 작성 순서는 무엇인지에 대해서도 같이 알아보려고 한다.
models.py
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=50)
class Category(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name='category')
name = models.CharField(max_length=50)
class Post(models.Model):
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True,
related_name='post')
title = models.CharField(max_length=50)
content = models.CharField(max_length=500)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
앞으로 나올 여러 개의 연습 문제에서의 역참조를 위해 이전 포스팅의 모델에서 related_name을 추가적으로 설정하였다.
역참조 : 클래스명_set 과 related_name의 차이
related_name을 설정해줌으로써 역참조를 선언한 이름으로 가능하게 해준다. 이를 설정하지 않는다면 클래스명_set으로 default related_name이 설정된다. 이것이 지금 까지 역참조를 클래스명_set으로 사용할 수 있었던 이유다.
하지만 가끔 모델에 두 개 이상의 Foreign Key가 존재할 경우 자동으로 생성되는 이름이 겹치는 경우가 있으므로 이럴 때는 related_name을 꼭 명시해주어야 한다.
Queryset 연습 1
>>> queryset = Category.objects.prefetch_related('post') # post_set 대신 post 사용
>>>
>>> queryset
SELECT "mysite2_category"."id",
"mysite2_category"."blog_id",
"mysite2_category"."name"
FROM "mysite2_category"
LIMIT 21
Execution time: 0.000147s [Database: default]
SELECT "mysite2_post"."id",
"mysite2_post"."category_id",
"mysite2_post"."title",
"mysite2_post"."content",
"mysite2_post"."created_at",
"mysite2_post"."updated_at"
FROM "mysite2_post"
WHERE "mysite2_post"."category_id" IN (1, 2, 3)
Execution time: 0.001465s [Database: default]
<QuerySet [<Category: Category object (1)>, <Category: Category object (2)>,
<Category: Category object (3)>]>
prefetch_related() 부분에 선언한 필드(post의 집합) 때문에 메인 쿼리 이후 한 개의 추가 쿼리가 발생하게 되었다.
Queryset 연습 2
>>> queryset = Post.objects.select_related('category').filter(category=1)
>>>
>>> queryset
SELECT "mysite2_post"."id",
"mysite2_post"."category_id",
"mysite2_post"."title",
"mysite2_post"."content",
"mysite2_post"."created_at",
"mysite2_post"."updated_at",
"mysite2_category"."id",
"mysite2_category"."blog_id",
"mysite2_category"."name"
FROM "mysite2_post"
INNER JOIN "mysite2_category"
ON ("mysite2_post"."category_id" = "mysite2_category"."id")
WHERE "mysite2_post"."category_id" = 1
LIMIT 21
Execution time: 0.000168s [Database: default]
<QuerySet [<Post: Post object (1)>, <Post: Post object (2)>,
<Post: Post object (3)>, <Post: Post object (4)>, <Post: Post object (5)>]>
select_related() 부분에 선언한 필드 때문에 메인쿼리에서 JOIN이 발생하게 되었다. 추가 쿼리는 발생하지 않았다.
필드를 여러 개로 줄 경우 JOIN은 필드의 갯수에 비례해 늘어나게 된다.
Queryset 심화 연습 1
>>> queryset = Category.objects.prefetch_related(Prefetch('post',
queryset=Post.objects.select_related('category').all()))
>>>
>>> queryset
SELECT "mysite2_category"."id",
"mysite2_category"."blog_id",
"mysite2_category"."name"
FROM "mysite2_category"
LIMIT 21
Execution time: 0.000113s [Database: default]
SELECT "mysite2_post"."id",
"mysite2_post"."category_id",
"mysite2_post"."title",
"mysite2_post"."content",
"mysite2_post"."created_at",
"mysite2_post"."updated_at",
"mysite2_category"."id",
"mysite2_category"."blog_id",
"mysite2_category"."name"
FROM "mysite2_post"
INNER JOIN "mysite2_category"
ON ("mysite2_post"."category_id" = "mysite2_category"."id")
WHERE "mysite2_post"."category_id" IN (1, 2, 3)
Execution time: 0.000245s [Database: default]
<QuerySet [<Category: Category object (1)>, <Category: Category object (2)>,
<Category: Category object (3)>]>
이번에는 prefetch_related() 안에서 Prefetch()를 통해 쿼리셋을 재 선언하였고, 그 안에서 select_related()를 이용하였다.
쿼리문 자체만 보면 매우 복잡하고 어렵게 보이지만 실제로 생성되는 SQL문을 보면 쉽게 이해할 수 있다.
- 메인 쿼리로서 Category 모델의 정보를 가져오는 것
- prefetch로 인해 추가 쿼리가 발생하였고 쿼리셋의 재 선언을 통해 select_related() 옵션을 주었으므로 추가쿼리에서의 JOIN 발생
즉, prefetch_related() -> 추가쿼리 / select_related() -> JOIN 발생의 규칙을 알고 있다면 생성되는 SQL을 예측할 수 있다.
Queryset 심화 연습 2
>>> queryset = Blog.objects.filter(id=1).prefetch_related(Prefetch('category__post',
queryset=Post.objects.filter(title__contains='python')))
>>>
>>> queryset
SELECT "mysite2_blog"."id",
"mysite2_blog"."name"
FROM "mysite2_blog"
WHERE "mysite2_blog"."id" = 1
LIMIT 21
Execution time: 0.000094s [Database: default]
SELECT "mysite2_category"."id",
"mysite2_category"."blog_id",
"mysite2_category"."name"
FROM "mysite2_category"
WHERE "mysite2_category"."blog_id" IN (1)
Execution time: 0.000068s [Database: default]
SELECT "mysite2_post"."id",
"mysite2_post"."category_id",
"mysite2_post"."title",
"mysite2_post"."content",
"mysite2_post"."created_at",
"mysite2_post"."updated_at"
FROM "mysite2_post"
WHERE ("mysite2_post"."title" LIKE '%python%' ESCAPE '\'
AND "mysite2_post"."category_id" IN (1, 2, 3))
Execution time: 0.000206s [Database: default]
<QuerySet [<Blog: Blog object (1)>]>
이전의 쿼리에서 약간 더 심화된 쿼리이다. Prefetch의 필드를 category__post로 정함으로써 post 모델에 관련된 정보까지 미리 가져오도록 하였다. 그렇기 때문에 총 3개의 쿼리가 발생하였다.
- 메인 쿼리로서 Blog 모델의 정보를 가져오는 것
- 추가 쿼리 1 - Category 모델의 정보를 가져오는 쿼리
- 추가 쿼리 2 - Post 모델의 정보를 가져오는 쿼리 (title로 필터를 주었으므로 where절 발생)
Queryset 심화 연습 3
>>> queryset = Blog.objects.filter(id=1).prefetch_related(
Prefetch('category', queryset=Category.objects.filter(name__contains='programming')),
Prefetch('category__post', queryset=Post.objects.filter(title__contains='python')))
>>>
>>> queryset
SELECT "mysite2_blog"."id",
"mysite2_blog"."name"
FROM "mysite2_blog"
WHERE "mysite2_blog"."id" = 1
LIMIT 21
Execution time: 0.000121s [Database: default]
SELECT "mysite2_category"."id",
"mysite2_category"."blog_id",
"mysite2_category"."name"
FROM "mysite2_category"
WHERE ("mysite2_category"."name" LIKE '%programming%' ESCAPE '\'
AND "mysite2_category"."blog_id" IN (1))
Execution time: 0.000175s [Database: default]
SELECT "mysite2_post"."id",
"mysite2_post"."category_id",
"mysite2_post"."title",
"mysite2_post"."content",
"mysite2_post"."created_at",
"mysite2_post"."updated_at"
FROM "mysite2_post"
WHERE ("mysite2_post"."title" LIKE '%python%' ESCAPE '\' AND "mysite2_post"."category_id" IN (1))
Execution time: 0.000210s [Database: default]
<QuerySet [<Blog: Blog object (1)>]>
>>>
바로 이전에 알아본 것과 굉장히 유사한 쿼리가 생성된 것을 볼 수 있다. 이번 예시에서 한 가지 달라진 점은 바로 Category와 Post에 대한 조건절을 Prefetch()를 두번 사용해 따로 분리해주었다는 점이다. 이렇게 하면 모델에 대한 조건절을 각각 부여할 수 있다.
다양한 예시를 통하여 ORM이 생성하는 쿼리셋의 구조에 대해서 조금이나마 익히게 되었을 것이다. 그렇다면 이제 ORM이 생성하는 쿼리셋의 구조와, 추천하는 ORM의 옵션 작성 순서에 대해 소개할 것이다.
ORM이 생성하는 SQL 구조 / 추천하는 ORM 작성 순서
queryset = (Model.objects
.select_related('정방향_참조필드1,','정방향_참조필드2',....)
# N개 만큼 JOIN 함
.annotate(커스텀속성1=F('모델필드'),
커스텀속성2=Case(
When(모델필드__isnull=False,
# when : case의 조건절, filter에 관련한 옵션 전부 사용 가능
then=Count('모델필드')),
# 모델 필드에서 Count() 함수를 질의함
default=Value(0, output_field=IntegerField(),
# output_field=장고에서 무슨 타입으로 결과를 받을 지 선언하는 부분
),
))
.filter(필터옵션)
.prefetch_related(
Prefetch('역방향_참조필드',
# 추가 쿼리 발생, 쿼리셋의 재 선언을 통해 다양한 튜닝 가능
queryset=(역방향_참조모델.objects
.select_related('역방향_참조모델의_정방향참조모델')
.filter(역방향_각종_질의문))
# .prefetch_related('역방향_참조모델의_역(정)방향참조모델')
# 위처럼 사용도 가능함
)
)
)
SELECT *
모델필드 AS 커스텀 속성1,
CASE
WHEN 모델필드 IS NOT NULL
THEN COUNT('모델필드')
ELSE 0 END AS 커스텀 속성2, # IntegerField()는 쿼리 영향 X
FROM `메인쿼리 Model`
LEFT INNER JOIN '정방향 참조필드1'
# INNER, OUTER 는 ForignKey의 null 옵션 값에 의해 결정
ON (~~~~)
LEFT OUTER JOIN '정방향 참조필드2'
# INNER, OUTER 는 ForignKey의 null 옵션 값에 의해 결정
ON (~~~~)
WHERE (필터 옵션)
SELECT *
FROM 역방향_참조모델
INNER JOIN '역방향_참조모델의_정방향참조모델'
ON ( )
WHERE (역방향_각종_질의문 AND 메인쿼리의_Model.`related_id` IN (1,2,3,4,....));
위와 같은 순서로 ORM을 작성하는 것이 실제로 생성되는 SQL의 순서와 가장 유사하다. 유심히 살펴보면 같은 옵션을 여러 번 사용하면 대부분 비슷한 구조로 SQL이 생성된다. 하지만 모든 경우에 같은 구조를 생성한다는 것은 보장할 수 없으므로 유독 느려지거나(서브쿼리, 슬로우쿼리) 하는 부분에 대해서는 실제로 어떻게 수행되는지 꼭 디버깅 해 볼 필요가 있을 것이다.
이번 포스팅에서는 ORM이 호출하는 SQL의 정형화 된 구조에 대해서 알아보았다. 다음 포스팅에서는 실수하기 쉬운 queryset의 예시들과 특성에 대해 알아볼 것이다.
지금까지 작성한 코드는 해당 레포지토리의 queryset_3 브랜치에서 확인할 수 있습니다
github.com/na86421/Django-ORM-Queryset/tree/queryset_3
공부하면서 정리한 내용이니 틀린 부분이 있을 수 있습니다. 댓글로 남겨주시면 감사하겠습니다.
'SW개발 > Django' 카테고리의 다른 글
[Django]FBV vs CBV (함수형 뷰 vs 클래스형 뷰) (0) | 2021.05.03 |
---|---|
[Django]Django ORM, 실수하기 쉬운 Queryset의 특징 (4) | 2021.04.26 |
[Django]Django select_related() 와 prefetch_related() (2) | 2021.04.15 |
[Django]Django ORM과 QuerySet의 특징 (LazyLoading, Caching, EagerLoading) (2) | 2021.04.12 |
[Django]Django REST Framework 튜토리얼 6 (ViewSets & Routers) (2) | 2021.04.06 |