PyconKorea | Django ORM QuerySet 구조와 원리 그리고 최적화 전략 강의 정리

Jihun Kim·2022년 1월 22일
3

파이콘

목록 보기
2/2
post-thumbnail
post-custom-banner

김성렬 님의 "Django ORM QuerySet 구조와 원리 그리고 최적화 전략강의"를 듣고 정리한 글입니다.

QuerySet을 통해 알아보는 ORM 특징

Lazy Loading(지연 로딩)

정말 필요한 시점에 SQL을 호출한다.

users: QuerySet = User.objects.all()
orders: QuerySet = Order.objects.all()

user_list = list(users)

(예시1)

  • 쿼리셋을 선언하면 선언 시점에는 쿼리셋에 지나지 않음
    • 리스트 모델이 넘어오는 것이 아니다.
  • 실제로 유저 리스트가 되는 시점은 쿼리셋을 리스트로 묶어줬을 때 인데, 이 때 QuerySet이 동작을 해서 SQL이 수행되어 우리가 원하는 데이터를 가지고 오는 것이다.
    • 즉, list()로 묶는 시점에 SQL이 실제로 수행되고 데이터를 가져오게되는 것이다.
  • 만약 선언 후 사용하지 않으면 SQL이 호출되지 않는다.
    • 따라서, 위의 orders는 list()로 묶지 않았으므로 SQL이 호출되지 않는다.

Django ORM은 정말 필요한 만큼만 호출한다

이러한 특성 때문에, ORM이 비효율적으로 동작하기도 함.

users: QuerySet = User.objects.all()

first_user: User = users[0]

user_list: list(User) = list(users)

(예시2)

  • 만약 users 쿼리셋에서 0번째 유저만 가져오고 싶다면 users[0]을 호출하게 된다.
    • 그러면 이 때 ORM은 user 1명만 얻기 위해서 LIMIT 1 옵션을 걸고 SQL을 호출한다.
  • 그러나, 그 뒤에 users를 list로 묶으면 모든 user 목록을 얻기 위해, 이 전에 LIMIT 1로 users[0]을 호출했었다는 점은 무시하고 다시 SQL을 호출한다.
    • 개발자라면 해당 로직에서 SQL을 한 번만 호출해서 데이터를 재사용하면 된다는 것을 알지만, 쿼리셋은 이를 알지 못하기 때문에 딱 필요한 만큼의 데이터만 가져오게 되는 것이다.


Caching

ORM이 필요한 시점에 필요한 만큼만 데이터를 가져오기 때문에 캐싱된 쿼리셋을 재사용 해야 한다.

users: QuerySet = User.objects.all()

user_list: list(User) = list(users)  # 모든 유저 정보가 캐싱 된다.

first_user: User = users[0]

(예시3)

  • 예시2에서 순서를 바꾸어 first_user를 먼저 호출하는 것이 아니라 모든 User를 QuerySet에서 먼저 호출한다.
    • 순서만 바꿀 뿐이다.
    • 이 때, SQL을 다시 호출하지 않고 0번째 유저를 캐시에서 가져오게 됨
  • 쿼리셋은 캐싱을 하기 때문에 호출하는 순서가 바뀌는 것만으로도 SQL이 달라질 수 있다.


Eager Loading(즉시 로딩)

QuerySet은 기본적으로 Lazy Loading이라는 전략을 택하는데, 가끔 SQL로 한 번에 많은 데이터를 끌어와야 할 때가 있다. 이를 ORM에서는 Eager Loading 이라고 한다.

→ QuetySet에서 이를 지원하기 위해 select_related, prefetch_related 메소드를 제공한다.

users: QuerySet = User.objects.all()  

for user in users:
    user.userinfo

(예제4: N+1 Problem이 발생하는 예제)

  • users 테이블과 userinfo 테이블이 1:1 관계인 상황이다.
  • users QuerySet을 선언해 놓고 user에서 userinfo를 가져오려고 하면 QuerySet의 기본 전략은 LazyLoading이기 때문에 모든 user의 정보를 SQL로 한 번에 가져왔다고 하더라도 userinfo 테이블에 있는 정보는 QuerySet을 선언한 시점에서 당장 필요하지 않기 때문에 데이터를 가져오지 않게 된다.
  • 따라서, for문이 돌 때마다 userinfo 테이블에 있는 정보를 가져오기 위해 쿼리가 계속 나가게 됨
    • for문이 도는 시점에 users의 모든 정보를 가져오기 위해 SQL이 호출 된다.

    • 그 다음 for문 안에서 user에 해당하는 userinfo를 QuerySet에서 찾는데, QuerySet은 해당 정보를 들고 있지 않다.

    • 그러면 QuerySet은 해당 정보를 찾아 SQL을 한 번 더 호출하게 된다.

      ⇒ 따라서, SQL이 n번 동안 계속해서 호출 되며, N+1개의 SQL 호출이 발생해 N+1 problem에 해당한다.

  • N+1 problem은 ORM에서 Lazy-Loading에 의해 발생하는 대표적인 문제이다.
  • userinfo라는 정보가 users QuerySet이 선언되는 시점에 당장 필요하지 않아 호출이 지연되었고, 이로 인해 user의 수만큼 userinfo SQL이 추가적으로 발생하게 된다.
    • 이러한 상황에서는 Eager-Loading 전략을 택하도록 QuerySet에 옵션을 줘야 한다.


QuerySet 상세

QuerySet의 구성요소

QuerySet은 한 개의 Query와 0 또는 N개의 추가 QuerySet으로 구성되어 있다.

# Query()는 개발자가 정의한 QuerySet을 읽어서 실제 SQL을 생성해 주는 구현체
from django.db.model.sql import Query  

class QuerySet:
	# 강의에서 김성렬 님은 이를 메인쿼리라 명명하였다.
	query: Query = Query()  
	
	# SQL의 수행 결과 저장 및 재사용(QuerySet Cache)
	# QuerySet 재호출시 해당 프로퍼티에 저장된 데이터가 없으면 SQL을 호출해 데이터를 가져온다.
	_result_cache: list[Dict[Any, Any]] = dict()

	# 추가 QuerySet이 될 타겟들 저장
	_prefetch_related_lookups: Tuple(str) = ()

	# SQL 결과값을 파이썬이 어떤 자료구조로 반환 받을 지 선언하는 프로퍼티
        # 이 값은 직접 수정하지 않으며 QuerySet.values() 또는 .values_list()를 사용해 변환시킨다.
	_iterable_class = ModelIterable

(예시4)

  • _result_cache
    • QuerySet은 최소 하나의 Query를 가지고 있다(메인 쿼리).
      • 이 때, _result_cache에 QuerySet이 가져온 캐싱하는 데이터들을 저장해 놓는다.
    • QuerySet을 호출할 때 _result_cache에 원하는 데이터가 없다면 그 때 SQL을 호출한다.
  • prefetch_related_lookups
    • 해당 변수에는 추가 QuerySet이 될 타겟들을 저장한다.
  • _iterable_class
    • QuerySet에 반환 타입을 어떤 방식으로 변환할 것인지에 관한 내용이 저장된다.
    • 일반적으로 추가적인 옵션을 주지 않으면 Django의 Model을 반환하게 된다.
    • 직접 수정하지는 않지만, values 옵션을 주면 dict 형태로 반환한다.
    • values_list 옵션을 주면 tuple list 형태로 데이터 값을 반환한다.


select_related는 join을 통해 데이터를 즉시 로딩하는 방식을 가지고 있으며 정방향 참조 필드이다. prefetch_related는 추가 쿼리를 수행해(쿼리를 하나 더 호출) 데이터를 즉시 가져오는 방식으로 역방향 참조 필드이다.

정방향/역방향 참조필드

  • 식당 : 주문 = 1 : N 관계일 때,
  • 식당 입장에서 주문은 역(방향) 참조 모델
  • 주문 입장에서 식당은 정방향 참조 모델

주문과 상품이 N : M(다대다) 관계라고 가정해 보자.(회원 : 주문 = 1 : N)

order_list = (
    Order.objects.select_related("order_owner")
    .filter(order_owner__username="username4")
    .prefetch_related("product_set_included_order")
)

(예시5)

  • 즉시 로딩 전략
    • Order의 리스트를 가지고 올 때 select_related 옵션을 주어 User의 정보를 조인하라는 옵션을 준다.
    • prefetch_related를 이용해 추가 쿼리를 통해 상품의 정보를 전부 다 끌어올 수 있다.
  • 역참조 모델일 경우 select_related 에 옵션을 줄 수가 없다.
    • 즉, 상품의 정보를 위의 select_related()에 추가할 수 없다.
    • 하지만, 그 역은 가능하다.
  • ‘order_owner’를 prefetch_related에 옵션으로 줄 수는 있지만 ‘product_set’이라는 필드를 select_related에는 줄 수가 없다.
    • 이는 장고에 제약이 있는 부분으로 역방향 참조 모델은 select_related에는 줄 수가 없지만 정방향 참조 모델은 select_relatedprefetch_related 모두에 옵션으로 줄 수 있다.


prefetch_related()는 추가 쿼리셋이다.

QuerySet은 한 개의 쿼리와 N개의 쿼리셋으로 이루어져 있다.

prefetch_related는 새로운 쿼리셋이고 그 안에 선언한 개수만큼 쿼리가 추가적으로 더 호출된다.

# 1번
queryset = AModel.objects.prefetch_related("b_model_set", "c_models")

# 2번
from django.db.models import Prefetch

queryset = AModel.objects.prefetch_related(
    Prefetch(to_attr="b_model_set", queryset=BModel.objects.filter(is_deleted=False)),
    Prefetch(to_attr="c_models", queryset=CModel.objects.all()),
)

# SQL
select * from a_model;
select * from b_model where id in (~~~) and is_deleted is False;
select * from c_model where id in (~~~);

(예시6)

  • 1번과 2번은 똑같은 기능을 한다(다만 2번에서는 b_model_set을 가져올 때 조건을 추가했다).
    • 그 결과가 2번 아래에 SQL로 작성되어 있다.
    • 2번의 경우에는 사용할 쿼리셋을 재선언하기 위해 사용한다.
  • 메인 모델에 역참조된 모델들을 전부 조회하고 싶다면 단순히 역참조된 필드만 선언해 주면 된다.
    • 이 때, 해당 필드를 조회하기 위한 SQL이 추가적으로 더 수행 된다.
    • 위의 예시에서는 ‘b_model_set’과 ‘c_models’ 2가지를 조회하기 위해 2번 더 수행 된다.
  • prefetch_related 안에 선언한 개수 만큼 추가적으로 SQL이 실행 된다.

QuerySet 연습

order_product = OrderedProduct.objects.select_related(
    "related_order", "related_product"
).filter(related_order=4)

(연습1)

  • select_related에 두 개의 정참조 모델이 선언 되었기 때문에 두 개의 모델이 조인된 SQL이 발생하게 된다.


OrderedProduct.objects.filter(
    product_cnt__lt=30, related_order__description="주문의 상세내용입니다."
).prefetch_related(
    Prefetch("related_order", queryset=Order.objects.select_related("mileage").all())
)

(연습2)

  • prefetch_related()옵션에 의해 1개의 추가 쿼리가 더 발생 한다.
  • Prefetch 함수를 통해 쿼리셋을 재선언 해 주었는데, 재선언된 쿼리셋은 select_related() 옵션을 주었다.
    • 따라서, 추가 쿼리셋이 발생하는데 그 쿼리셋에 join하는 옵션인 select_related가 붙어 있는 것이다.
    • 즉, Mileage라는 모델을 join한 추가 쿼리 셋이 하나 더 발생하게 된다.


OrderedProduct.objects.filter(product_cnt__gt=23).prefetch_related(
    Prefetch(
        "related_product__product_owned_company",
        queryset=Company.objects.filter(name__contains="comanpy_name"),
    )
)

(연습3)

  • ‘related_product’에 역참조된 ‘product_owned_company’를 한 번 더 조인하기 때문에prefetch_related()에 2개의 옵션이 추가된 셈이다.
    • 따라서, 두 개의 추가 쿼리가 발생하게 된다.
    • 또한, company를 타겟으로 하는 QuerySet을 name__contains 조건으로 재선언을 해서 WHERE 절을 걸었으므로 SQL 쿼리에서는 LIKE 문이 붙게 된다.
  • 만약, ‘related_product’의 쿼리셋을 제어하고 싶다면 QuerySet을 따로 조개서 다시 재선언 하면 된다.
    OrderedProduct.objects.filter(product_cnt__gt=23).prefetch_related(
        Prefetch(
    	"related_product", queryset=Product.objects.filter(price__isnull=False)
        )
        Prefetch(
            "related_product__product_owned_company",
            queryset=Company.objects.filter(name__contains="comanpy_name"),
        )
    )


SQL Performance를 체크하기 위한 TestCase [CaptureQueriesContext]

김성렬님이 자주 사용하시는 테스트 케이스로 소개하신 것이 CaptureQueriesContext이다.

강의 내용에 따르면, N+1 문제로 인한 크리티컬한 성능 이슈만 체크하고 싶을 때 사용하기에 유용하다고 한다.

꿀팁인 것 같다.

아래와 같은 예시를 강의에서 확인할 수 있었다.

from django.test.utils import CaptureQueriesContext
from rest_framework.test import APIClient

def test_check_n_plus_1_problem():
	from django.db import connection
	
	with CaptureQueriesContext(connection) as expected_num_queries:
		APIClient.get(path='/restaurants/")
	
	# 주문이 두 개 더 추가된 이후 API에서 발생하는 SQL Count
	Order.objects.create(
		total_pricee=1000,
	)
	Order.objects.create(
		total_pricee=5000,
	)
	
	with CaptureQueriesContext(connection) as checked_num_queries:
		APIClient.get(path='/restaurants/")

	# 이제 주문이 두 개 더 발생했다고 SQL이 2개 더 생성되었는지 여부를 확인한다.
	# 주문이 N개 생성되었다고 해서 SQL이 N개 더 생성되면 안된다! 
	# 즉, 아래의 두 쿼리셋의 길이가 같아야 한다.
	assert len(checked_num_queries) == len(expected_num_queries)


실수하기 쉬운 QuerySet의 특성들

prefetch_related()와 filter()는 완전 별개다

비효율적인 쿼리

company_qs = Company.objects.prefetch_related("product_set").filter(
    name="company_name1", product__name__isnull=False
)

(예시7)

  • 이 예제는 잘못된 쿼리를 사용했다.
  • QuerySet은 한 개의 쿼리와 N개의 추가 QuerySet으로 구성되어 있다.
  • filter()는 한 개의 쿼리에 해당하는 내용들을 제어한다.
  • prefetch_related()는 추가 쿼리셋에 있는 내용들을 제어한다.
  • 여기서는 product__name__isnull=False 라는 조건절을 검색하기 위해서 필연적으로 product를 조인할 수밖에 없다.
    • 따라서, 조인을 통해 데이터를 불필요하게 한 번 더 검색을 하고 prefetch_related 라는 옵션을 줬기 때문에 product가 한 번 더 쿼리를 생성하게 된다.

해결 방법

  • 해결방법1: prefetch_related() 옵션 제거
    • QuerySet이 알아서 JOIN으로 SQL을 풀어 준다.

    • 즉, 쿼리 한 줄로 데이터를 전부 끌어올 수 있다.

      company_qs = Company.objects.filter(
          name="company_name1", product__name__isnull=False
      )

      (예시7-1)

  • 해결방법2: filter에 넣어줬던 product 관련 조건절을 Prefetch()에 제공
    • 추가 쿼리에 where문이 추가 된다.

      company_qs = Company.objects.filter(name="company_name1").prefetch_related(
          "product_set",
          Prefetch(queryset=Product.objects.filter(product__name__isnull=False)),
      )

      (예시7-2)



강의에서 추천하는 QuerySet 작성 순서

김성렬님이 개인적으로 추천하는 작성 순서이다.

  • Model을 놓고 annotate, select_related, filter 그리고 prefetch_related 순서대로 QuerySet을 작성하는 것이 좋다.
    • 이 순서가 실제로 발생하는 SQL의 순서와 가장 유사하다.
  • 다 떠나서, 가장 주의해야 하는 것은prefetch_related가 filter 앞에 있는 것은 피하는 것이 좋다는 것이다.
    • 앞에서 봤듯이, prefetch_related가 붙어 있고 그 다음에 filter가 붙어 있게 되면 마치 하나의 쿼리에서 이것들이 동작하는 것처럼 착각하기 쉽기 때문이다.


QuerySet 캐시를 재활용하지 못하는 QuerySet 호출

company_list = list(Company.objects.prefetch_related("product_set").all())
company = company_list[0]

company.product_set.all()  # SQL이 추가 발생하지 않음(이미 Eager Loading 했기 때문)

company.product_set.filter(name="불닭볶음")  # SQL이 추가 발생

# SQL을 추가로 발생시키지 않기 위한 방법 - list comprehension
fire_noodle_product_list = [
    product for product in company.product_set.all() if product.name == "불닭볶음"
]

(예시8)

  • company_list[0]에서 쿼리가 추가적으로 나가지 않는다.
    • 왜냐하면, 앞에서 이미 모든 Comapny의 정보가 캐싱되어 있기 때문이다.
  • compnay.product_set.all()에서도 쿼리가 추가적으로 나가지 않는다.
    • 왜냐하면, prefetch_related()를 통해서 product_set을 이미 Eager-Loading으로 한 번에 다 끌어왔기 때문이다.
  • 그러나, company.product_set.filter(name='불닭볶음')에서는 쿼리가 추가적으로 발생한다.
    • 이 경우에는 QuerySet의 캐시를 재사용 하지 않고 SQL을 무조건 호출하게 된다.
    • 이는 아마 장고에서 의도한 동작일 것으로 보인다(by 김성렬님)
  • 따라서, SQL을 추가적으로 발생시키지 않으려면 company.product_set.all()을 호출해서 그 안에 있는 로직에서 파이썬 list comprehension으로 찾아 주는 방식을 사용해야 한다.
    • 이 경우 추가쿼리가 발생하지 않는다! 착각하기 쉬운 부분이다.


RawQuerySet은 NativeSQL이 아니다.

가끔 원하는 SQL을 위해 QuerySet을 완전 포기할 때가 있다.

즉, Django connection에서 cursor.execute() 식으로 로직을 사용할 때가 있다.

강의에서는 이 방법 보다는 RawQuerySet을 추천한다.

from django.db.models.query import QuerySet, RawQuerySet

raw_queryset: RawQuerySet = Model.objects.raw("select * from model where ~~")
queryset: QuerySet = Model.objects.filter(~~)

(예시9)

  • RawQuerySet과 쿼리셋은 크게 다르지는 않다.
    • 메인 쿼리를 Native SQL로 구현한다는 차이점만 존재한다.
    • 나머지는 대부분의 것들이 동일하고 RawQuerySet 또한 아직 ORM의 제어권 안쪽에 있는 구현체이다.
  • raw()를 선언하면 RawQuerySet이 된다.
  • QuerySet은 메인 쿼리를 작성할 때 장고에서 제공하는 sql.Query()를 사용하는 반면, RawQuerySet은 장고에서 제공하는 것을 사용하지 않고 사용자가 직접 제공해준 Raw SQL을 가져다가 그대로 사용한다.
    # 쿼리셋
    class QuerySet:
    
    	def __init__(self, model=None, query=None, using=None, hints=None):
    		...
    		self._query = query or sql.Query(self.model)
    		...
    
    # RawQuerySet
    class RawQuerySet:
    
    	def __init__(self, raw_query, model=None, query=None, params=None, translations=None,
    							using=None, hints=None):
    		...
    		self._query = query or sql.RawQuery(sql=raw_query, using=self.db, params=params)
    		...
    (예시9-1)
  • RawQuerySet은 QuerySet의 또다른 유형이기 때문에 prefetch_related()를 사용할 수 있다.
    from django.db.models.query import RawQuerySet
    
    from django.db.models.query import QuerySet
    
    # 로우쿼리셋
    order_queryset: RawQuerySet = Order.objects.raw(
        raw_query="""
            SELECT * 
            FROM "orm_practice_app_order" 
            INNER JOIN "orm_practice_app_user" 
            ON ("orm_pratice_app_order"."order_owner_id" = "orm_practice_app_user"."id") 
            WHERE "orm_pratice_app_user"."username" = %(username_param1)%
            """
            , params=("username_param1": "username4")
    ).prefetch_related("product_set_included_order")
    
    # 쿼리셋
    order_queryset: QuerySet = (
        Order.objects.select_related("order_owner")
        .filter(order_owner__username="username4")
        .prefetch_related("product_set__included_order")
    )
    (예시9-2)
  • Models.objects.raw()를 사용하면 아래 메소드들은 사용할 수 없다.
    • 여기에 있는 메소드들은 메인 쿼리를 제어하기 위해 사용되는 메소드들이기 때문에 NativeSQL로 작성해 주어야 한다(raw 옵션 안에서).
      .select_related()  # 메인 쿼리에 JOIN 옵션을 주는 메소드
      FilteredRelation()  # ON절 제어 옵션 - Join이 안되므로 당연히 사용 불가
      .annotate()  # 메인쿼리에 AS 옵션을 주는 메소드
      .order_by()  # 메인 쿼리에 order by 옵션 주는 메소드
      .extra()  # 메인 쿼리에 sql을 추가 반영하는 메소드
      [:10]...[:2]...  # 메인 쿼리에 limit 옵션을 걸 수 없다.


서브쿼리의 발생 조건: QuerySet In QuerySet 문제

일반적으로 서브쿼리는 슬로우 쿼리를 많이 야기한다(심한 경우 서비스 자체가 느려질 수도 있다). 따라서, 서브쿼리를 의도하고 사용하는 케이스가 생각보다 별로 없는데 가끔씩 쿼리셋이 개발자의 의도와 다르게 혹은 예상치 못하게 서브쿼리를 수행할 때가 있다.

django orm에 서브쿼리 옵션이 있긴 하지만 이 옵션을 주지 않았을 때도 가끔씩 발생할 수 있다.

company_queryset: QuerySet = Company.objects.filter(id__lte=20).values_list("id", flat=True)

product_queryset: QuerySet = Product.objects.filter(product_owned_company__id__in=company_queryset)

(예시10)

  • QuerySet 안에 QuerySet이 있으면 서브쿼리가 발생할 수 있다.
  • 예시10에서 company_queryset이 아직 쿼리셋이며 실행되지 않았기 때문에 product_queryset 안에 조건절로 들어갔을 때도 쿼리셋 상태이다.
    • 따라서, 두 쿼리셋이 합쳐서 수행되어 서브쿼리가 발생했다.
  • 이런 케이스들을 막기 위해서는 QuerySet을 그 즉시 수행하도록 로직을 작성해야 한다.
    • list() 안에 넣는 방법이 그 중 하나이다. list() 옵션으로 QuerySet을 바로 수행하면 company_queryset은 쿼리셋이 아니게 된다.

      company_queryset: QuerySet = list(Company.objects.filter(id__lte=20).values_list("id", flat=True)
      ) 
      product_queryset: QuerySet = Product.objects.filter(product_owned_company__id__in=company_queryset)


서브쿼리의 발생 조건: exclude() 조건절의 함정(역방향 참조모델 정상동작 예)

normal_joined_queryset = Order.objects.filter(description__isnull=False, product_set_included_order__name='asd')

(예시11)

  • exclude()는 not 옵션, filter()는 바른 옵션으로 주로 사용한다.
  • 그런데, 가끔씩 exclude() 옵션에서 서브쿼리가 발생할 때가 있다.
  • 위에서 filter 조건에서 주었던 옵션을 exclude()로 옮겨 주기만 했는데 서브쿼리가 발생한다.
    normal_joined_queryset = Order.objects.filter(description__isnull=False).exclude(product_set_included_order__name='asd')
    • 즉, filter 조건에 넣어서 사용하면 JOIN이 되는데 exclude에 넣어서 사용하면 서브쿼리가 발생한다.
  • 마찬가지로, filter절에 ~Q 옵션(Q는 'OR' 조건에 사용한다. 따라서 아래의 경우 ~인 것 또는 ~아닌 것을 의미한다)을 주어도 여전히 서브쿼리가 발생한다.
    normal_joined_queryset = Order.objects.filter(Q(description__isnull=False), ~Q(product_set_included_order__name='asd'))
  • 또한, select_related 옵션을 추가해 강제로 JOIN을 유도해도 여전히 서브쿼리가 발생한다.
  • 따라서, 이 경우 차선책으로prefetch_related(Prefetch())를 사용해서 따로 제약을 주는 방법을 대체할 수 있다.


서브쿼리의 발생 조건: exclude() 조건절의 함정(정방향 참조모델 정상동작 예)

정방향 참조의 경우 위의 역방향 참조에서와는 달리 .exclude() 절을 사용할 경우 의도한 대로 JOIN을 수행한다. 이를 참고하면 서브쿼리를 발생시키지 않을 수 있을 것이다.

normal_joined_queryset = Order.objects.filter(description__isnull=False).exclude(order_owner__userinfo__tel_num='010-0000-0000')

(예시12)



QuerySet의 다양한 반환 타입 values(), values_list()

values_list()의 경우 flat=True 옵션과 named=True 옵션이 있다.

# ModelIterable
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)

# NamedValuesListIterable: django에서 제공하는 Raw라는 객체에 데이터를 담아 리턴
result: List[Raw] = Model.objects.values_list(named=True)

(예시13)

  • named=True 옵션은 2.X에 들어와서 생긴 옵션이다.
    • 이를 이용하면 반환되는 값이 namedtuple이다.


values(), values_list() 사용시 주의점: EagerLoading 옵션을 무시함

values(), values_list() 를 사용하면 eager-loading 옵션들을 전부 무시하는 특성이 있다. 따라서, 해당 QuerySet에 주어진 select_related(), prefetch_related() 옵션들을 전부 무시한다.

gg = list(Product.objects.select_related('product_owned_company').filter(id=1).values())

(예시 14)

  • 이 경우 select_related JOIN을 하지 않으며 select_related를 무시한다.
  • values에 정말 JOIN 해야만 가져올 수 있는 데이터를 명시해야 조인을 한다.
    gg = list(Product.objects.select_related('product_owned_company').filter(id=1).values(product_owned_company)
  • 그러나, 이는 어찌 보면 당연한 결과이다. 왜냐하면 values()values_list()는 DB의 row 단위로 데이터를 반환하기 때문이다.
    • 즉, 객체와 관계지향 간에 매핑이 일어나지 않기 때문에 ORM의 EagerLoading 개념의 구현체인 select_related()prefetch_related()는 DB Row 단위로 데이터를 조회하는 values(), values_list()에서는 무의미한 옵션인 것이다.


QuerySet을 잘 사용하는 방법

  1. QuerySet은 1개의 Query와 0~N개의 QuerySet으로 이루어져 있다.
  2. 수행하고자 하는 SQL보다 가져오고자 하는 데이터 리스트를 먼저 떠올리자.
  3. QuerySet이 제공하는 SQL 구조를 벗어난다면 RawQuerySet으로 풀자.
  4. 단조로운 SQL 작업을 줄일 수 있으며, Object와 Relational을 Mapping해 준다는 ORM의 장점을 얻을 수 없다면 NativeSQL을 사용하자.
  5. NativeSQL 사용을 망설이지 말라.
    • 특히 SQL 성능이 중요한 경우라면 가끔씩은 Django ORM으로 원하는 쿼리 결과를 얻을 수 없을 때도 있다.
    • 또한, 가독성을 높이기 위해 사용하는 것이 좋을 수도 있다.


너무 좋은 강의였고, 나한테 꼭 필요한 강의였다!





참고

Django ORM (QuerySet)구조와 원리 그리고 최적화전략 - 김성렬 - PyCon Korea 2020

김성렬님이 링크 올려두신 요기요 기술 블로그

profile
쿄쿄
post-custom-banner

0개의 댓글