SerializerMethodField() 사용의 쿼리 최적화
안녕하세요 똑똑한 개발자에서 백엔드 개발을 하고 있는 jujun입니다.
SerializerMethodField는 원하는 key value를 만들 수 있게 도와주지만 잘못 사용하면 N+1 문제를 발생시킵니다.
.
.
본론
예제 모델
Profile 모델과 Essay 모델이있고 Essay는 Profile을 참조하고 있습니다.
# profile/models.py
class Profile(models.Model):
username = models.CharField("제목", max_length=50)
# essay/Essay.py
class Essay(models.Model):
title = models.CharField("제목", max_length=50)
profile = models.ForeignKey(Profile, verbose_name='작성자', on_delete=models.CASCADE)
.
. ###정방향 참조
- 모든 essay의 내용과 작성자 정보를 담은 json을 반환하고자 합니다.
해당 뷰는 모든 Essay를 반환합니다.
# view.py
class EssayListAPIView(ListAPIView):
serializer_class = EssayListSerializer
queryset = Essay.objects.all()
.
.
Essay의 작성자를 정보와 Essay를 직렬화합니다.
# seralizers.py
class EssayListSerializer(serializers.ModelSerializer):
profile = serializers.SerializerMethodField()
class Meta:
model = Essay
fields = ['id', 'title', 'profile']
def get_profile(self, obj):
return obj.profile.username
. 반환값입니다. 모든 essay 와 작성자 정보를 반환했습니다.
[
{
"id": 1,
"title": "첫번째 에세이",
"profile": "James"
},
{
"id": 2,
"title": "두번째 에세이",
"profile": "Tana"
},
{
"id": 3,
"title": "세번째 에세이",
"profile": "Rara"
}
]
. .
발생한 쿼리 입니다. 전체 Essay를 가져오는 쿼리가 한 번, 각 Essay인스턴스가 Profile을 호출하는 쿼리가 한 번씩 총 네번 쿼리가 발생했습니다.
(0.000) SELECT "essay_essay"."id", "essay_essay"."title", "essay_essay"."profile_id" DESC; args=()
(0.000) SELECT "profile_profile"."id", "profile_profile"."username" WHERE "profile_profile"."id" = 1 LIMIT 21; args=(1,)
(0.000) SELECT "profile_profile"."id", "profile_profile"."username" WHERE "profile_profile"."id" = 2 LIMIT 21; args=(2,)
(0.000) SELECT "profile_profile"."id", "profile_profile"."username" WHERE "profile_profile"."id" = 5 LIMIT 21; args=(5,)
HTTP GET /api/v1/essay/ 200 [0.02, 127.0.0.1:35170]
. . ###해결
view에서 반환하는 queryset에 select_related를 사용하였습니다.
# view.py
class EssayListAPIView(ListAPIView):
queryset = Essay.objects.select_related('profile').all()
profile의 내용이 join된 한번의 쿼리가 발생했습니다.
(0.001) SELECT "essay_essay"."id", "essay_essay"."title", "essay_essay"."profile_id", "profile_profile"."id", "profile_profile"."username" FROM "essay_essay" INNER JOIN "profile_profile" ON ("essay_essay"."profile_id" = "profile_profile"."id"); args=()
HTTP GET /api/v1/essay/hint/ 200 [0.02, 127.0.0.1:35178]
. . .
###역방향 참조
- 사용자가 작성한 essay를 반환합니다.
.
prefetch_related()로 essay_set 쿼리셋을 만들어줍니다.
# view
class ReverseAPIview(ListAPIView):
queryset = Profile.objects.prefetch_related('essay_set').all()
serializer_class = ReverseSerializer
prefetch_related된 essay_set의 값만 반환합니다.
# seralizer
class ReverseSerializer(serializers.ModelSerializer):
essay = serializers.SerializerMethodField()
class Meta:
model = Profile
fields = ['id', 'username', 'essay']
def get_essay(self, obj):
return obj.essay_set.values()
반환값입니다.
[
{
"id": 1,
"username": "James",
"essay": [
{
"id": 1,
"title": "첫번째 에세이"
}
]
},
{
"id": 2,
"username": "Tana",
"essay": [
{
"id": 2,
"title": "두번째 에세이"
}
]
},
{
"id": 3,
"username": "Taro",
"essay": []
},
{
"id": 4,
"username": "Kakao",
"essay": []
},
{
"id": 5,
"username": "Rara",
"essay": [
{
"id": 3,
"title": "세번째 에세이"
}
]
}
]
. .
쿼리입니다. profile 테이블을 가져오는 쿼리 essay 테이블을 가져오는 쿼리가 발생했습니다. 하지만 각 profile에 맞는 쿼리를 가져오기 위해 다섯 번의 쿼리가 추가적으로 발생했습니다.
(0.000) SELECT "profile_profile"."id", "profile_profile"."username" FROM "profile_profile"; args=()
(0.000) SELECT "essay_essay"."id", "essay_essay"."title", "essay_essay"."profile_id" FROM "essay_essay" WHERE "essay_essay"."profile_id" IN (1, 2, 3, 4, 5); args=(1, 2, 3, 4, 5)
(0.000) SELECT "essay_essay"."id", "essay_essay"."title", "essay_essay"."profile_id" FROM "essay_essay" WHERE "essay_essay"."profile_id" = 1; args=(1,)
(0.000) SELECT "essay_essay"."id", "essay_essay"."title", "essay_essay"."profile_id" FROM "essay_essay" WHERE "essay_essay"."profile_id" = 2; args=(2,)
(0.000) SELECT "essay_essay"."id", "essay_essay"."title", "essay_essay"."profile_id" FROM "essay_essay" WHERE "essay_essay"."profile_id" = 3; args=(3,)
(0.000) SELECT "essay_essay"."id", "essay_essay"."title", "essay_essay"."profile_id" FROM "essay_essay" WHERE "essay_essay"."profile_id" = 4; args=(4,)
(0.000) SELECT "essay_essay"."id", "essay_essay"."title", "essay_essay"."profile_id" FROM "essay_essay" WHERE "essay_essay"."profile_id" = 5; args=(5,)
HTTP GET /api/v1/essay/reverse/ 200 [0.02, 127.0.0.1:35288]
. . ###해결
뷰입니다. Prefetch로 essay_set의 인스턴스를 profile 인스턴스에 달아주었습니다.
# view
class ReverseAPIview(ListAPIView):
queryset = Profile.objects.prefetch_related(
Prefetch('essay_set',
queryset=Essay.objects.all() ,
to_attr='essays')).all()
serializer_class = ReverseSerializer
시리얼라이저 입니다. 새로운 serialzer를 선언하여 Prefetch로 호출된 essays 를 처리하였습니다.
# serializer
class EssaySerializer(serializers.ModelSerializer):
class Meta:
model = Essay
fields = ['id', 'title']
class ReverseSerializer(serializers.ModelSerializer):
essay = serializers.SerializerMethodField()
class Meta:
model = Profile
fields = ['id', 'username', 'essay']
def get_essay(self, obj):
qr = [ EssaySerializer(essay).data for essay in obj.essays]
return qr
. 발생한 쿼리입니다. prfile 테이블과 essay 테이블을 각각 호출하였습니다.
(0.001) SELECT "profile_profile"."id", "profile_profile"."username" FROM "profile_profile"; args=()
(0.000) SELECT "essay_essay"."id", "essay_essay"."title", "essay_essay"."profile_id" FROM "essay_essay" WHERE "essay_essay"."profile_id" IN (1, 2, 3, 4, 5); args=(1, 2, 3, 4, 5)
HTTP GET /api/v1/essay/reverse/ 200 [0.02, 127.0.0.1:35420]
. .
마치는 말
DRF를 사용하는데 많은 쿼리를 발생시키는 것은 좋지 않습니다. django의 lazy loading과 Eager loading을 이해하는데 좋은 예제가 되었으면 좋겠습니다. 부족한 내용 끝까지 봐주셔서 감사합니다.