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을 이해하는데 좋은 예제가 되었으면 좋겠습니다. 부족한 내용 끝까지 봐주셔서 감사합니다.

jujun's profile image

jujun

2021-04-28 10:35

Read more posts by this author