ORM 최적화

14180 단어 djangoTILTIL

Lazy loading

Queryset은 정말 필요한 시점에 필요한 만큼만 호출되는 LazyLoading 이라는 특징을 가지고 있다. 해당 데이터가 지금 당장 필요하지 않으면 호출하지 않는다.


그렇다면 실제 언제 SQL문이 호출 될까?
실제로 필요한 순간 출력하거나 값을 저장하거나 참조할 때 해당값을 db에서 loading 한다.

  • Iteration
  • slicing
  • Picking/Caching
  • repr()
  • len()
  • list()
  • bool()



iteration

Queryset은 iterable하고, 반복문에서 queryset의 첫 row가 순회될 때, database에 호출이 일어난다 그리고 결과가 print 되기 전에 cache에 저장된다.

queryset = Entry.objects.all()    

# evaluated and cached
for each in queryset:
    print(each.headline)

# cache된 데이터를 사용한다.
for each in queryset:
    print(each.headline)



Slicing

evaluated 되지 않은 queryset을 Slicing하면 새로운 queryset을 반환한다. 반환된 queryset은 추가적인 수정(filter or ordering)을 허용하지 않는다. 다만 slicing을 더할 수는 있다.

# You can't use filter to queryset anymore.
queryset = Entry.objects.all()[10:100] 


# You can use filter to q1 but not to q2, q3.
q1 = Entry.objects.all() 
q2 = q1[1:10] 
q3 = q2[1:5] 


# saves results to cache of q1 
lst1 = [each.blog.id for each in q1] 


# saves results to cache of q2 
lst2 = [each.blog.id for each in q2]

만약 벌써 evaluated된 queryset을 slicing한다면 queryset이 아닌 list가 반환 된다. evaluation 후에는 queryset은 cache 값을 사용하기 때문이다.

queryset = Entry.objects.all() 
lst = list(queryset) 

# returns a list of entry objects 
first_ten = queryset[:10] 

# list slicing not queryset slicing because first_ten is a list. 
first_five = first_ten[:5] 

evaluated 되지 않은 queryset으로 부터 인덱스를 사용해 한 원소를 가져온다면, database에 요청이 가지만 만약 벌써 evaluated된 queryset을 가져온다면 cache를 사용한다.
queryset = Entry.objects.all() 
# Queries the database because queryset hasn't been evaluated yet. 
print(queryset[5]) 

lst = list(queryset) 
# Using cache because evaluation happened in previous list() operation. 
print(queryset[5]) 
print(queryset[10]) 

예외가 있는데, 만약 python slice 문법 중
step parameter를 evaluated 되지 않은 Queryset에 사용하면 그 경우, 즉시 database로 query가 가고 queryset이 아닌 list가 반환 된다.

entry_list = Entry.objects.all()[1:100:2]



repr()

repr() method는 객체의 반환한다
repr() method를 호출하여 Queryset이 evaluated 되었다면, 그 결과는 cache 되지 않느다.

# repr() evaluates but does not saves results to cache. 
queryset = Entry.objects.all() 
str_repr = repr(queryset) 

# Not using cache.Hitting database again. 
for each in queryset: 
	print(each.headline)

Note : print 함수 또한 Queryset을 evaluate 시키지만 cache에 저장하지 않는다



len()

len을 불러 Queryset이 평가되면
결과는 evaluated된 결과는 cache에 저장된다

# len() evaluates and saves results to cache. 
queryset = Entry.objects.all() ln = len(queryset) 

#Using cache from previous evaluation. 
for each in queryset: 
    	print(each.headline)

list()

list 를 사용하여 evaluated된 queryset은 list를 반환하고 그 결과를 cache에 저장한다.

# Evaluates the queryset and saves results in cache. 
queryset = Entry.objects.all() 
lst = list(queryset) 

# Using cache from previous list() evaluation. 
for each in queryset: 
	print(each.headline)



Lazy loading의 문제점

  • 여러개의 queryset이 합쳐져 한꺼번에 실행되면 매우 느리게 동작할 수 있다.
  • 중복되는 queryset이 반복적으로 호출 될 수 있다.
  • N+1 problem

N + 1 problem

from django.db import models

class Person(models.Model):
	name = models.CharField(max_length = 20)
    age  = models.IntegerField()
    
class Dog(models.Model):
	name   = models.CharField(max_length = 20)
    age    = models.IntegerField()
    owner = models.ForeignKey('Person', on_delete = models.CASCADE)

위의 모델이 있을 때, 다음이 작성했다고 하자


dogs = Dog.objects.all()

for dog in dogs:
	dog.owner

모든 dog들을 조회하기 위해 SQL이 1번 호출되고, for 문안에서 매번 owner인 Person의 정보를 매번 조회하기 위해 SQL문이 N번 호출 된다.

모든 Dog의 정보를 한 번에 가져 왔더라도, owner의 정보는 가져오지 않았으므로 이를 가져오기 위한 SQL문이 dog이 바뀔 때 마다 N번 호출 되는 것이다.




Eager loading

Eager loading(즉시 로딩)은 lazy loading(지연 로딩)과 반대되는 개념으로 즉시 로딩 시키는 방식이다.

두 가지 방식으로 eager loading 한다

select_related : ForeignKey, OnetoOneField 관계인 모델들을 함께 가져온다 (1:1, 1:N 관계에서 1 or N이 사용할 수 있다 역참조 불가)
SQL문의 JOIN을 이용해 연결된 모델을 같이 불러온다.

prefetch_related : ForeignKey, OnetoOneField 관계 뿐만 아니라 ManytoManyField 에서 사용가능 하다 (M : N 관계에서 1:N에서 1이 사용할 수 있다)

좋은 웹페이지 즐겨찾기