Django ORM 최적화
[PyCon Korea 2020] Django ORM (QuerySet) 구조와 원리 그리고 최적화 전략
QuerySet을 통해 알아보는 ORM의 특징
쿼리셋
은 1개의 메인 쿼리
와 0~N개의 추가 쿼리셋
으로 구성
- Lazy Loading
- 쿼리셋을 선언할 때는 쿼리셋 자체로만 존재하며 실제 SQL이 실행되는 시점은 그 쿼리셋이 사용되었을 때
- 당장 필요하지 않으면 호출을 지연하는 특성으로 꼭 필요한 시점에만 필요한 만큼 SQL이 호출됨
- 재사용하지 못함으로 불필요한 쿼리가 더 호출될 수 있음
- 호출하는 순서만 바뀌는 것으로 쿼리셋 캐싱을 통해 개선 가능
- Caching
- 쿼리셋 캐싱을 재사용할 수 있음
- 모든 user를 먼저 캐싱해두고, 필요한 특정 유저 정보를 가져올 때 앞의 변수를 활용하면 추가 쿼리를 호출하지 않음
- Eager Loading (즉시로딩, N+1 Problem)
- for문이 돌면서 조회할 때마다 SQL이 계속 호출되는 문제를 N+1 Problem이라고 함
- user가 100명 있으면 for문으로 userinfo를 얻기 위해서는 총 100+1번 쿼리가 호출되는 현상
- (유저를 호출하는 sql 한번) + (유저 개수 N) = (N+1개의 쿼리)
- 즉시로딩을 하기위해, 즉, (N+1) 해결을 위해 Django에서는
select_realated()
와 prefetched_related()
메서드를 사용 가능
- 호출할 때
result cache
에 원하는 데이터가 없으면 쿼리셋 호출
select_realated()
: 조인을 통해 즉시로딩
prefetched_related()
: 추가 쿼리를 사용하여 즉시로딩
prefetched_related
에 선언한 속성 개수만큼 쿼리셋이 추가로 호출됨
- 역참조는
select_related
옵션을 줄 수 없음 (Django에서 제약이 있는 부분)
- 테스트 할 때
assertNumQueries()
로 테스트 케이스를 작성하는 경우가 많지만, 매번 체크를 해줘야하고 N+1문제로 인한 크리티컬한 성능 이슈만 커버하기 때문에 captureQueriesContext
를 활용하는 것도 도움됨
QuerySet 사용에서 실수하기 쉬운 점들
주요 변수
_result_cache_
- 호출된 데이터를 저장하는 변수
- 만약 찾는 데이터가 여기 없으면 추가 쿼리셋 수행
prefetch_related_lookups
iterable_class
- 쿼리셋의 반환 타입 결정(default는 Model)
주의 사항
prefetch_related()
와 filter()
는 완전 별개
prefetched_related()
는 추가 쿼리셋에서 제어
filter()
는 새로운 쿼리셋이 아니라 한 개 쿼리셋 안에서 제어
- filter의 조건절은 WHERE절을 붙히기 위해 메인쿼리에서 INNER JOIN을 사용하게 되고, 추가 쿼리에서 한번 더 조회하므로 비효율적
annotate
→ select_related
→ filter
→ prefetch_related
순서가 실제 SQL 순서와 가장 유사하므로 이 순서로 쿼리셋을 작성하는 것을 추천
- 쿼리셋 캐시를 재활용하지 못할 때가 있음
.all()
로 질의하면 캐시를 재활용하지만, 특정 상품을 찾으려고 하면 캐시를 재사용하지 않고 SQL로 질의
- 쿼리셋을 재호출하지 않으려면
.all()
로 불러온 것에서 if절을 활용하여 리스트 컴프리헨션으로 빼오는 것을 추천
raw 쿼리셋
은 쿼리셋의 또 다른 유형이기 때문에 prefetch_related()
, Prefetch()
사용이 가능