lazy loading
orm은 lazy loading의 특징이 있는데 필요할때만 sql을 호출하는 것이다.
class MainPageView(View):
def get(self, request):
def get_list(products):
product_list = [{
'id' : product.id,
'name' : product.name,
'price' : product.price,
'is_green': product.is_green,
'is_best' : product.is_best,
'images' : [{
'id' : image.id,
'url': image.url
} for image in product.productimage_set.all()]
} for product in products]
return product_list
new_products = Product.objects.all().order_by('-created_at')[:8]
best_products = Product.objects.filter(is_best = True).order_by('-created_at')[:8]
green_products = Product.objects.filter(is_green = True).order_by('-created_at')[:8]
return JsonResponse({
'new_products' : get_list(new_products),
'best_products' : get_list(best_products),
'green_products': get_list(green_products)}, status=200)
위의 MainPageView의 get함수가 호출되면 호출되는 sql쿼리의 수는 27개이다.
new_products = Product.objects.all().order_by('-created_at')[:8]
위 코드로 쿼리 호출이 일어나는지 찍어보면 Number of Queries : 0 이라고 뜬다.
new_products라는 쿼리셋을 선언하는 것만으로는 orm은 sql을 호출하지 않는다.
리턴을 하면서 get_list함수가 실행되는데 get_list함수 실행시 new_products에 담긴 product1,2,3...들에 접근해서 id, 이름 등 데이터를 가져오기 위해서 쿼리셋을 사용할때 쿼리셋은 sql을 호출하게 된다.
n+1 problem
lazy loading으로 인해 발생하는 문제
처음에 new_products를 선언할 때는 sql을 호출하지 않고,
get_list안에서 products의 객체 하나씩 for문을 돌려야 하는데 선언할때 호출한 sql이 없기 때문에 그때 최신순으로 정렬된 8개의 products를 가져오는 sql을 한번 호출한다.
그리고 products 안의 product에 하나씩 접근하기 위해서 객체를 하나씩 불러오는 쿼리셋을 계속 호출한다.
SELECT `products`.`id`, `products`.`created_at`, `products`.`updated_at`, `products`.`name`, `products`.`price`, `products`.`is_green`, `products`.`is_best`, `products`.`category_id` FROM `products` ORDER BY `products`.`created_at` DESC LIMIT 8; args=(); alias=default
SELECT `product_images`.`id`, `product_images`.`url`, `product_images`.`product_id` FROM `product_images` WHERE `product_images`.`product_id` = 42; args=(42,); alias=default
SELECT `product_images`.`id`, `product_images`.`url`, `product_images`.`product_id` FROM `product_images` WHERE `product_images`.`product_id` = 41; args=(41,); alias=default
SELECT `product_images`.`id`, `product_images`.`url`, `product_images`.`product_id` FROM `product_images` WHERE `product_images`.`product_id` = 40; args=(40,); alias=default
SELECT `product_images`.`id`, `product_images`.`url`, `product_images`.`product_id` FROM `product_images` WHERE `product_images`.`product_id` = 39; args=(39,); alias=default
SELECT `product_images`.`id`, `product_images`.`url`, `product_images`.`product_id` FROM `product_images` WHERE `product_images`.`product_id` = 38; args=(38,); alias=default
SELECT `product_images`.`id`, `product_images`.`url`, `product_images`.`product_id` FROM `product_images` WHERE `product_images`.`product_id` = 37; args=(37,); alias=default
SELECT `product_images`.`id`, `product_images`.`url`, `product_images`.`product_id` FROM `product_images` WHERE `product_images`.`product_id` = 36; args=(36,); alias=default
SELECT `product_images`.`id`, `product_images`.`url`, `product_images`.`product_id` FROM `product_images` WHERE `product_images`.`product_id` = 35; args=(35,); alias=default
총 product수가 8(n)개니까
product를 호출하는 sql 8번 호출 + products호출 sql 1번 = 9(8+1)번의 sql이 호출하는데 이걸 n+1프라블럼이라고 한다.
똑같이 동작하는 쿼리셋이 new_products, best_products, green_products 세개가 있어서 해당 뷰에 get요청이 오면 sql쿼리가 27번 호출되는 것이다.
캐싱
쿼리셋은 이미 호출한 sql에 대해서 캐싱을 해놓고 다시 같은 범위에서 요청이 오면 sql을 다시 호출하지 않고 캐시에서 가져오게 된다.
그래서 코드의 순서에 따라서 호출되는 sql수가 달라질 수 있다.
eager loading
lazy loading의 n+1 problem을 해결해줄 즉시로딩 방식이다.
쿼리셋은 sql을 캐싱하기 때문에 처음에 즉시로딩 방식으로 앞으로 사용하게 될 sql을 한번에 호출해서 캐시에 저장되도록 하면 그 후로 새로 sql을 호출하지 않고 동작하게 할 수 있다.
장고 쿼리셋 api에서는 select_related, prefetch_related라는 메소드로 eager loading을 지원한다.
select_related는 조인을 해서 데이터를 즉시 로딩하고(정참조만 가능)
prefetch_related는 추가쿼리를 수행해서 데이터를 즉시 로딩한다.(역참조, 정참조 모두 가능)
적용
new_products = Product.objects.all().prefetch_related('productimage_set').order_by('-created_at')[:8]
return JsonResponse({'new_products' : get_list(new_products)}, status=200)
for문 안에서 상품-이미지의 관계를 이용해서 호출해야 하니까 미리 즉시로딩으로 캐싱해 놓기 위해 prefetch_related를 사용한다.
SELECT `products`.`id`, `products`.`created_at`, `products`.`updated_at`, `products`.`name`, `products`.`price`, `products`.`is_green`, `products`.`is_best`, `products`.`category_id` FROM `products` ORDER BY `products`.`created_at` DESC LIMIT 8; args=(); alias=default
SELECT `product_images`.`id`, `product_images`.`url`, `product_images`.`product_id` FROM `product_images` WHERE `product_images`.`product_id` IN (42, 41, 40, 39, 38, 37, 36, 35); args=(42, 41, 40, 39, 38, 37, 36, 35); alias=default
Number of Queries : 2
prefetch_related는 추가쿼리를 수행하기 때문에 products에 대한 쿼리, products하나씩에 해당하는 images를 불러오는 쿼리 두개가 호출된다.
best, green에도 적용하면
new_products = Product.objects.all().prefetch_related('productimage_set').order_by('-created_at')[:8]
best_products = Product.objects.filter(is_best = True).prefetch_related('productimage_set').order_by('-created_at')[:8]
green_products = Product.objects.filter(is_green = True).prefetch_related('productimage_set').order_by('-created_at')[:8]
return JsonResponse({
'new_products' : get_list(new_products),
'best_products' : get_list(best_products),
'green_products': get_list(green_products)}, status=200)
이렇게 되고, 디버거로 확인해보면 쿼리수가 27->6개로 줄어들은 것을 확인할 수 있다.
ProductListView도 같은 방법으로
products = products.prefetch_related('productimage_set').order_by(sort_by[request.GET.get('sorting', None)])[ offset : offset + limit ]
원래 있던 product에 .prefetch_related('productimage_set')만 추가해주면 쿼리수가 13(한페이지12개 + 1)->4로 줄어든다!
'wecode' 카테고리의 다른 글
| 마이허니트립 | 데이터베이스 모델링 (0) | 2022.08.04 |
|---|---|
| 구방문방구 | 프로젝트 회고 (1) | 2022.08.01 |
| 구방문방구 | 상품목록 페이지네이션(offset, limit), 검색 (0) | 2022.07.30 |
| 구방문방구 | 상품목록 정렬하기(query parameter) (0) | 2022.07.30 |
| 구방문방구 | 메인페이지, 상품상세페이지 api (0) | 2022.07.30 |