오늘도 개발
Django ORM 최적화 - select_related, prefetch_related 본문
Django ORM이란?
파이썬 언어를 sql문으로 바꿔주는 django의 번역기.
Django ORM이 실행하는 sql을 보려면 settings.py에 logging 설정을 해주면 된다.
logging은 개발 시에만 사용하고 배포는 하면 안 된다.(속도가 저하됨)
Debug 모드가 True일 때만 log가 찍히게 설정해야 한다.
Django ORM의 특징
LazyLoading(지연 로딩)
쿼리셋을 정의하는 시점에는 SQL 쿼리를 준비만 해놓고,
쿼리셋이 평가(evaluation)되는 시점(=사용되는 시점)에 SQL 쿼리를 사용해서 DB를 호출하는 것.
쿼리셋이 평가되는 경우 : slicing, iteration, repr(), len(), list() 등
publishers = Publisher.objects.all()
# SQL 쿼리를 준비만 하고 실행하지는 않은 상태. 로그에 아무것도 안 뜸
# ‘SELECT * FROM publishers’라는 쿼리 생성
# publishers.query
# 생성한 쿼리 ‘SELECT * FROM publishers’를 저장해두는 곳
print(publishers)
# 쿼리셋을 평가하는 시점. publishers.query에 저장된 쿼리를 실행함
# db에서 ‘SELECT * FROM publishers’ 실행
Caching
DB 호출로 얻은 결과(=쿼리셋)를 임시 저장소에 저장해서(=캐싱)
쿼리셋을 사용할 때 매번 DB를 호출하지 않아도 되게 해주는 기능.
쿼리셋은 다음과 같은 경우 자동으로 캐싱된다.
1) 쿼리셋을 리스트로 바꾸는 경우
2) 쿼리셋에 for 문을 실행하는 순간
우선 쿼리셋이 캐싱되지 않는 경우를 살펴보자.
다음 코드는 쿼리셋을 리스트로 바꾸지도 않았고, 쿼리셋을 for문으로 접근하지도 않았기 때문에
Publisher.objects.all()의 결과가 캐싱되지 않는다.
따라서 인덱스로 쿼리셋을 evaluation할 때마다 DB가 호출된다 => 성능이 저하된다.
publishers = Publisher.objects.all()
# 'SELECT * FROM publishers'라는 쿼리 준비
publishers[0]
# DB 호출해서 'SELECT * FROM publishers' 쿼리 실행한 후 첫번째 항목 가져옴
publishers[1]
# DB 호출해서 'SELECT * FROM publishers' 쿼리 실행한 후 두번째 항목 가져옴
publishers[2]
# DB 호출해서 'SELECT * FROM publishers' 쿼리 실행한 후 세번째 항목 가져옴
publishers[3]
# DB 호출해서 'SELECT * FROM publishers' 쿼리 실행한 후 네번째 항목 가져옴
publishers[4]
# DB 호출해서 'SELECT * FROM publishers' 쿼리 실행한 후 다섯번째 항목 가져옴
# 총 db 호출 5회
다음은 쿼리셋이 캐싱되는 경우이다.
쿼리셋을 리스트로 만들었으므로 Publisher.objects.all()로 가져온 쿼리셋이 캐싱된다.
이후 인덱스로 쿼리셋을 evaluation하면 캐시에서 항목을 가져오므로 DB를 호출하지 않는다.
publishers = Publisher.objects.all()
# 'SELECT * FROM publishers'라는 쿼리 준비
list(publishers)
# DB 호출해서 'SELECT * FROM publishers' 쿼리 실행한 후 결과를 캐싱함
# publishers._result_cache
# 캐시가 저장되는 곳
publishers[0]
# 캐시된 쿼리셋의 첫번째 항목을 가져옴
publishers[1]
# 캐시된 쿼리셋의 두번째 항목을 가져옴
publishers[2]
# 캐시된 쿼리셋의 세번째 항목을 가져옴
publishers[3]
# 캐시된 쿼리셋의 네번째 항목을 가져옴
publishers[4]
# 캐시된 쿼리셋의 다섯번째 항목을 가져옴
# 총 db 호출 1회
EagerLoading
N+1 Problem
테이블을 캐싱할 때 그 테이블과 연결된 테이블은 캐싱되지 않음.
따라서 참조하는 테이블의 정보를 사용할 때 캐시가 없으므로 계속 db를 호출하게 됨 => 성능 저하
<models.py>
class Brand(models.Model):
name = models.CharField(max_length=45)
class Product(TimeStampModel):
title = models.CharField(max_length=45)
main_image = models.URLField(max_length=300)
price = models.DecimalField(decimal_places=3, max_digits=10)
brand = models.ForeignKey(Brand, on_delete=models.CASCADE)
<views.py>
products = Product.objects.all()
# 'SELECT * FROM products'라는 쿼리 준비
for product in products:
# DB에서 'SELECT * FROM products' 쿼리 실행하고 캐싱
print(product.brand.name)
# Brand 테이블은 캐싱한 적 없으므로 쿼리 생성해서 DB 호출
# books 오브젝트가 10개 있으면 총 DB 호출 10회
EagerLoading
N+1 문제의 해결 방식.
연결된 테이블의 데이터까지 같이 캐싱함.
select_relaed, prefetch_related 메서드로 구현할 수 있음.
Select_related
정참조 시 사용.
두 테이블을 db에서 Inner join해서 한번에 가져옴(총 DB 호출 1회).
select_related()의 괄호 안에는 정참조하는 필드명을 적어준다.
books = Book.objects.all().select_related('publisher')
# Book과 Publisher 테이블을 inner join해서 가져오는 쿼리 준비
for book in books:
# DB에서 쿼리 실행 후 가져온 결과를 캐싱
print(book.publisher.name)
# sql 쿼리 총 1회 실행
Prefetch_related
역참조 시, 또는 ManyToManyField에서 사용.
두 테이블을 db에서 각각 가져오고(총 DB 호출 2회) 장고에서 합친다.
prefetch_related()의 괄호 안에는 참조하는 필드명을 적어준다.
stores = Store.objects.all().prefetch_related('books')
# stores 테이블을 가져오는 쿼리 준비
# books 테이블을 가져오는 쿼리 준비
for store in stores:
# DB 호출해서 stores 테이블을 가져옴
# DB 호출해서 books 테이블을 가져옴
# 두 테이블을 장고에서 합친 뒤 캐싱
print([book.name for book in store.books.all()])
# sql 쿼리 총 2회 실행
문제점 - for문을 도는 중, 참조하는 테이블에서 filter()를 사용하면 또 db 호출을 쿼리셋 수만큼 실행
stores = Store.objects.all().prefetch_related('books')
# stores 테이블을 가져오는 쿼리 준비
# books 테이블을 가져오는 쿼리 준비
for store in stores:
# DB 호출해서 stores 테이블을 가져옴
# DB 호출해서 books 테이블을 가져옴
# 두 테이블을 장고에서 합친 뒤 캐싱
print([book.name for book in store.books.filter(name__startswith='해리')])
# DB 호출하고 books 테이블에서 해리로 시작하는 책이름 찾는 쿼리 실행
# store 오브젝트가 10개인 경우 sql 쿼리 총 12회 실행
해결 - Prefetch 객체 사용해서 참조하는 테이블에 사용할 filter()를 미리 저장해둠.
stores = Store.objects.all().prefetch_related(
Prefetch(
'books',
queryset = Book.objects.filter(name__startswith='해리'),
to_attr='filtered_books')
)
# stores 테이블을 가져오는 쿼리 준비
# books 테이블을 가져오는 쿼리 준비
# books 테이블에서 해리로 시작하는 책이름 찾는 쿼리 준비
for store in stores:
# DB 호출해서 stores 테이블을 가져옴
# DB 호출해서 books 테이블을 가져옴
# 두 테이블을 장고에서 합친 뒤 캐싱
# DB 호출해서 books 테이블에서 해리로 시작하는 책이름 찾는 쿼리 실행하고 결과 캐싱
print([book.name for book in store.filtered_books])
# store 개수가 아무리 많아도 sql 쿼리 총 3회 실행'웹 프로그래밍 > Django' 카테고리의 다른 글
| Django Rest Framework 튜토리얼 따라하기 2. view 쉽게 작성하기 (0) | 2023.01.08 |
|---|---|
| Django Rest Framework 튜토리얼 따라하기 1. Serialization (6) | 2023.01.07 |
| Django로 Unit Test해보기 (0) | 2022.08.05 |
| Django 앱 세팅하는 법 (0) | 2022.07.17 |
| Django 프로젝트 세팅하는 법 (0) | 2022.07.17 |