본문 바로가기
Programming/Python * Django

[Django] ORM 구조와 원리 그리고 최적화 전략 - Pycon2020 김성렬님 발표 정리

by 고막고막 2021. 6. 6.

QuerySet을 통해 알아보는 ORM의 특징

  • Lazy Loading : 쿼리셋을 선언할 때는 쿼리셋 자체로만 존재한다. 실제 SQL이 실행되는 시점은, 그 쿼리셋이 사용되었을 때이다. 당장 필요하지 않으면 호출을 지연하는 특성. 꼭 필요한 시점에만 필요한 만큼 SQL이 호출된다. 재사용하지 못함으로, 불필요한 쿼리가 더 호출될 수 있다.
  • Caching : QuerySet 캐싱을 재사용할 수 있음. 모든 user를 먼저 캐싱해두고, 필요한 특정 유저 정보를 가져올 때 앞의 변수를 활용하면 추가 쿼리를 호출하지 않는다.
  • Eager Loading(즉시로딩, N+1 Problem) : for문이 돌때마다 쿼리를 계속 돌린다. user가 100명 있으면 for문으로 userinfo를 얻기 위해서는 총 100+1번 쿼리가 호출되는 현상.

QuerySet 상세

  • filter() : where절
  • select_related() : JOIN
  • prefetch_related() : 새로운 추가 쿼리셋
  • CaptureQueriesContext : SQL Testcase 작성을 위한 유틸, 특정 쿼리셋에서 발생하는 쿼리의 갯수를 capture 할 수 있다.

실수하기 쉬운 QuerySet의 특성들

  • prefetch_related()와 filter()는 완전 별개이다. filter의 조건절은 where절을 붙히기 위해 메인쿼리에서 INNER JOIN을 사용하게 되고, 추가 쿼리에서 한번 더 조회하므로 비효율적이다.
  • 해결방안 : filter에 넣었던 product 관련 조건절을 Prefetch()에 제공 → 추가쿼리에 where 문이 수정됨
    company_queryset = (Company.objects
            .prefetched_related('product_set')
            .filter(name="company_name", product_name_isnull = False))
    cs
  • 추천하는 QuserySet의 순서 : annotate → selected_related → filter → only → prefetch_related (실제로 발생하는 SQL의 순서와 같음)
  • QuerySet 캐시를 재활용하지 못하는 queryset의 호출
    # 회사의 상품목록을 eager loading 함
    company_list = list(Company.objects.prefetch_related('product_set').all()))
    # company_list의 result_cache를 재활용하므로 쿼리 발생 X
    company = company_list[0]
    # filter()를 사용하면 result_cache를 재활용 X 쿼리를 수행 O
    company_list.filter(name="불닭볶음면")
    # .all()로 질의하면 result_cache를 재활용하므로 쿼리 발생 X
    fire_noodle_product_list = [product for product in company.product_set.all() if product_name="불닭볶음면"]
    cs
    • 서브쿼리 발생조건(Queryset in Queryset) : 일반적으로 서브쿼리는 slow query를 유도하기 때문에 지양하지만, 개발자의 의도와 다르게 쿼리셋을 사용하게 되는 경우가 있음.
      company_queryset : QuerySet = Company.objects.filter(id=20).values_list("id", flat=True)
      # company_queryset 로직이 여기서 수행되므로 의도치않은 서브쿼리 발생
      product_queryset : QuerySet = Product.pbjects.filter(company_id__in=company_queryset)
      # 해결방안 : list로 묶어서 queryset을 바로 수행해버리자.
      company_queryset : List[Company] = list(Company.objects.filter(id=20))
       
      # 같은 조건이라도 filter() 내에서는 JOIN으로 수행되지만,
      # 역방향참조모델의 exclude() 또는 filter내 ~Q()로 사용시 무조건, 서브쿼리가 발생.
      # 차선책 : prefetch_related(Prefetch()), 정방향참조모델에서는 JOIN으로 수행된다.
      cs
    • 쿼리셋의 다양한 리턴 타입 : values(), values_list()
      result : List[Model] = Model.objects.all()    
      # .only() : 지정한 필드만 조회 
      # .defer() : 지정한 필드만 제외하고 조회
       
      # ValuesIterable
      result : List[Dict[str, Any]] = Model.objects.values()
       
      # ValuesListIterable
      result : List[Tuple[str, Any]] = Model.objects.values_list()
       
      # FlatValuesListIterable
      result : List[Any] = Model.objects.values_list('pk', flat=True)
       
      # FlatValuesListIterable, django에서 제공하는 Raw 객체에 담아서 리턴
      result : List[Raw] = Model.objects.values_list(named=True)
      cs
  • values(), values_list() 사용시 DB Raw 단위 데이터를 반환하기 때문에 select_related()와 prefetch_related()의 eagger loading 옵션이 무시된다.

 

 

https://www.youtube.com/watch?v=EZgLfDrUlrk&t=460s

https://github.com/KimSoungRyoul/Django_ORM_pratice_project/tree/master

업무하면서 답답한 부분이 많이 해소되었다. 파이콘 좋은 강의 감사드립니다.