IT/Django

Django query 최적화(select_related, prefetch_related, Prefetch + @ django_auto_prefetching)

bepuri 2022. 5. 25. 11:46
728x90

django_auto_prefetchingdjango query 최적화를 할때

select_related와 prefetch_related 두개면 왠만한 중복쿼리들은 제거할 수 있다.

N+1 쿼리를 해결할 수 있는 간단한 참고자료들은 많은데 내 경우는 약간은 특이한 상황이였다.

 

원페이퍼라는 부동산 계약서비스에서 최근 매물 관리까지 할 수 잇는 서비스로 확장하다보니 DB도 복잡해지고, 백엔드도 점점 복잡해졌다. 그러다가 근래 매물 관리 기능을 추가하면서 쿼리 최적화에 대해 신경쓰지 못했었는데 매물 리스트 조회하는 페이지에 중복쿼리가 너무 많았다.

위와같이 중복쿼리가 많이 이유는 아래와 같이 serializer에서 쿼리를 호출해서 생기는 문제와,

SerializerMehtodField에서는 되도록 쿼리를 호출하면 안된다.

쿼리를 호출하면 serialize할때마다 쿼리가 호출되서 쿼리 효율이 매우 떨어진다.

    def get_author_profile(self, instance):
        author = instance.author
        profile = author.profiles.filter(is_activated=True).first()
        return ListingEveryoneProfileSerializer(profile).data

 

 

이문제를 해결하기전에 먼저 불필요하게 추가 호출되는 쿼리를 제거하려고 시도했다.

 

select_related와 prefetch_related의 사용법은 구글링을 통해 참고 바란다.

 

select_related를 통해서 매물 모델 1:1관계거나 정참조인 필드들을 먼저 가져왔다.

Listing.objects.select_related("author", "listingaddress").all()

수정한 쿼리셋은 위와 같고

쿼리 속도도 20%정도 빨라졌다.

그 뒤 prefetch_related를 통해서 역참조 관계인 모델들도 먼저 가져오려고 했으나,

author.profiles.filter를 어떻게 처리해줘야할지 난감했다.

단순히 author__profiles를 prefetch에 넣어주는것으로는 is_activated 조건을 줄 수가 없었고, is_activated 조건을 주기 위해서 SerializerMethodField를 사용하면 prefetch_related로 가져온 author 관련 쿼리들의 prefetch된 정보를 사용할수가 없었다.

 

SerializerMethodField에서 불필요한 쿼리들이 다시 생기는 것이다. 따라서 listing*** 관련된 모델들은 query 속도를 줄이는데 실질적인 영향을 미쳤지만 이후 author**관련 모델들은 prefetch로 인한 쿼리만 추가되고, 쿼리속도에는 오히려 부정적인 영향을 미쳤다.

Listing.objects.select_related("author", "listingaddress")
.prefetch_related(
    "listingimage_set",
    "listingitem_set",
    "listing_visits",
    "author__profiles",
    "author__profiles__address",
    "author__profiles__certification",
    "author__profiles__expert_profile",
)
.all()

Listing.objects.select_related("author", "listingaddress")
            .prefetch_related(
                "listingimage_set",
                "listingitem_set",
                "listing_visits",
            )
            .all()

author관련 참조모델들을 가져온 것이 SerializerMethodField에서 호출하는 쿼리셋때문에 의미가 없어지는것을 확인할 수 있다.

 

따라서 먼저 아래 코드를 author_profile을 SerailizerMethod형태가 아닌 다른 형태로 수정할 필요가 있었다.

...
    author_profile = serializers.SerializerMethodField()
...

	def get_author_profile(self, instance):
        author = instance.author
        profile = author.profiles.filter(is_activated=True).first()
        return ListingEveryoneProfileSerializer(profile).data

수정전 코드

serializers.py

...
author_profiles = ListingEveryoneProfileSerializer(
    source="author.profiles", many=True, read_only=True
)
...
views.py

Listing.objects.select_related("author", "listingaddress")
            .prefetch_related(
                "listingimage_set",
                "listingitem_set",
                "listing_visits",
                "author__profiles",
                "author__profiles__address",
                "author__profiles__certification",
                "author__profiles__expert_profile",
            )
            .all()

수정후 코드

SerializerMethodField를 없애주고 Profile정보를 보여주기 위한 Serializer를 하나 만들어주었다.

그 뒤 참조 모델을 source에 정의해주었다.

중복쿼리들이 모두 사라져 쿼리 수도 적어지고, 속도도 빨라진것을 확인할 수 있다.

 

그러나 처음 author_profile은 activated된 프로필 중 첫번째 프로필이였다. 이 prefetch 쿼리는 author와 관련된 모든 프로필을 호출하는 쿼리였다. 굳이 활성화되지 않은 프로필은 전달될 필요가 없기 때문에 is_activate=True라는 필드 필터를 넣고 싶었다.

 

여기서 문제에 봉착했는데,, 결과적으로 Prefetch를 사용하면 간단하게 해결할 수 있는 문제였다.

Listing.objects.select_related("author", "listingaddress")
            .prefetch_related(
                "listingimage_set",
                "listingitem_set",
                "listing_visits",
                Prefetch("author__profiles", queryset=Profile.objects.filter(is_activated=True)),
                "author__profiles__address",
                "author__profiles__certification",
                "author__profiles__expert_profile",
            )
            .all()

처음에 Prefetch기능을 보았으나 사용하지 않은 이유가 serializer에서 author.profiles.filter처럼

Profile.objects.filter(is_actvated=True, author=??) 담아주어야할 것이라 생각했는데, Pretch할때 들어간 역참조 모델이름을 통해 자동적으로 filter된다는 부분을 놓쳐서 헤매었다.

 

 Prefetch("author__profiles")

 

위 코드에서 이미 author id를 통해서 profiles이 이미 where 절을 통해 필터링 되었다.

 Prefetch("author__profiles", queryset=Profile.objects.filter(is_activated=True)),

이와같이 queryset에 필터를 추가해줄때 추가적인 where절이 추가되는 것을 확인할 수 있다.

요약하면 추가적인 질의가 필요하면 Prefetch를 그게 아니면 select_reated, prefetch_related면 충분히 쿼리 최적화하는데 문제가 없다.

그리고 주의할 것은 SerializerMethodField에서는 쿼리 질의를 실행하지 않는 것이 좋다.

왜냐면 List와 같은 Method(GET Method에서 목록을 호출하는 Method를 의미)는 인스턴스 숫자만큼 N+1 쿼리가 발생할 수 있기 때문이다.

 

PS. 다만 이렇게하면 생기는 단점이 Prefetch된 데이터가 serializer에서 first를 호출했을때처럼 하나의 오브젝트로 반환되는게 아니라 list로 넘어간다. 이건 queryset쪽에서 first를 호출하고, Serializer쪽에서 many=True 속성을 빼봐도 에러나는걸보면,, 방법이 없을듯하다. 그냥 frontend쪽에서 인덱싱해서 사용하는걸로 일단은 코드를 수정해놨다.

아무리 구글링 해봐도 알 수 없어서, 혹시 아시는분이 계시다면 조언 부탁드립니다.

 

Prefetch에서는 하나의 object를 리턴하는 방법은 없는듯하고,, queryset으로만 넘겨줘야하는것 같다. 그래서 나는 model 내부에 첫번째 프로필에 접근할 수 있는 프로퍼티를 하나 만들어주고, 해당 프로퍼티를 통해 mobile_number에 접근하는 형태로 하여 해결하였다.

serializers.py

class AskListingSerializer(ModelSerializer):
    ...
    mobile_number = PhoneNumberField(source="author.first_profile.mobile_number", read_only=True)
    ...
class User(...):
    @property
    def first_profile(self):
        return self.profiles.first()
...

그리고 최적화를 하는 과정에서 django-auto-prefetching이라는 파이썬 자동 프리패칭 패키지를 알게되었다.

적용방법은 패키지 github에 가면 나와있고, 만약 여러분이 serializer에서 property에 접근하는 형태로 source를 지정해주는 경우 get_auto_prefetch_excluded_fields 함수를 오버라이딩 해서 해당 property를 prefetch하지 않도록 해야한다. 왜냐하면 해당 property는 실제 db 스키마에는 없는 필드이기 때문이다.

728x90