Programming/Django

[Django] QuerySet 특징 - Lazy evaluation (Lazy evaluation with Django QuerySet)

sahayana 2022. 5. 6. 20:54

 


#1. Lazy evaluation / Lazy loading

 

 

장고 ORM의 QuerySet은 기본적으로 'Lazy' 합니다.

Lazy 하다는 것은 장고 objects manager를 통해 '선언'한 쿼리를 바로 '호출'하지 않는다는 것을 의미합니다.

간단한 테스트 예시를 통해 살펴보면,

def test_what_can_be_evaluated(self) -> None:    
        # When
        with self.assertNumQueries(0):
            User.objects.all()
            User.objects.filter(id=1)
            User.objects.order_by('-id')

이 테스트는 실제 호출한 쿼리가 0이 되어야 통과하는 테스트입니다.

문제 없이 통과했습니다.

 

장고의 QuerySet은 실제로 SQL 쿼리를 '호출'(Hit)해야만 하는 시점'필요한 만큼만' 호출합니다.

 


#2. Why Lazy?

 

Lazy evaluation은 어플리케이션의 병목현상을 줄이기위한 효율적인 방법으로 잘 알려져 있습니다.
기본적으로 웹 어플리케이션의 성능 저하 이유로 가장 큰 것이 데이터베이스에서 발생한다고 알고 있습니다.
괜히 백엔드 엔지니어들이 쿼리 횟수를 줄이는 것에 집착하는 것이 아닙니다.

user = User.objects.filter(Q(is_superuser=True) & Q(is_staff=True))
                    .filter(username__icontains='admin')

위 예시처럼 쿼리를 계속해서 연결(chaining)할 때마다 지속적으로 SQL 쿼리를 '호출'(Hit) 한다면 DB 레벨에서의
병목현상등의 심각한 성능저하를 일으키게 됩니다.

 

이런식으로 chaining 하는 과정에서 실제로 DB를 호출하지 않고 함수에 연결하거나 값을 return 받을 수 있기 때문에 매우 효율적입니다.

 


#3. When evaluated?

 

QuerySet은 다음의 경우에 SQL 쿼리를 호출합니다.

 

  • Iteration
  • Slicing ("step" 파라미터를 사용할 경우)
  • Pickling / Caching
  • repr()
  • len()
  • list()
  • bool()

 

이 밖에도 데이터를 불러오는 시점이 명확한 get(), count(), value(), value_list(), create() 등의 몇몇 ORM 메서드도 lazy하지 않습니다.

 


#4. QuerySet Caching

 

QuerySet은 evaluated되는 순간 불필요한 SQL호출을 줄이기 위해 캐싱을 사용합니다.

 

다음과 같은 경우는 캐싱을 하지 않아 매우 비효율적인 코드입니다.

all_user = User.objects.all()	# No evaluation at this point
first_user = all_user[0]	# Total NumQuery = 1
all_user = list(all_user) 	# Total NumQuery = 2

 

여기서 코드 순서를 다음과 같이 바꾸기만 하면 캐싱이 되어 SQL 호출을 줄일 수 있습니다.

all_user = User.objects.all()	# No evaluation at this point
all_user = list(all_user) 	# Total NumQuery = 1
first_user = all_user[0]	# Total NumQuery = 1

따라서 로직을 설계할 때 캐싱을 염두에 두고 쿼리문을 조합하는 것이 중요합니다.

 


#5. Lazy evaluation의 단점 - N+1 problem

 

Lazy loading을 기본으로하는 ORM의 대표적인 문제입니다.

N+1 Problem은 한번의 호출로 N개의 쿼리셋을 가져온 뒤 관련 컬럼을 얻기 위해 N개의 모델을 순회할때마다 쿼리를 계속해서 수행하는 매우 비효율적인 SQL 쿼리를 호출하는 문제를 의미합니다.

all_user = User.objects.all()
'''
 all_user 호출 쿼리 수행 1번, all_user 안의 컬럼 조회 호출 N번 = N+1 Problem 
 all_user 쿼리셋 갯수 만큼 계속해서 SQL쿼리를 호출한다. 매우 비효율적
'''
for user in all_user:
	posts = user.post_set.count()

위 예시에서 모든 유저데이터의 갯수가 천개, 만개라고 생각하면 끔찍합니다.

 

장고 ORM에서는 이를 해결하기 위한 수단으로 select_related()와 prefetch_releated() 메서드를 제공합니다.

관련해서 따로 포스팅을 남기고자 합니다.


#6. 정리

 

  • 장고의 QuerySet은 Lazy하다. (선언만 할뿐 실제로 호출하지는 않는다.)
  • 필요한 시점에 필요한 정보만 호출한다.
  • QuerySey 캐싱을 생각해 API설계를 하는 것이 좋다.

#6. 참고