users: QuerySet = User.objects.all()
위 코드를 통해 User의 Model이 넘어온다고 착각하는 경우가 많은데, 선언한 시점에는 여전히 QuerySet으로 남아있다.
list()로 쿼리셋을 묶는 로직이 수행될 때, 쿼리셋이 동작하여 SQL이 호출되고 데이터가 불러와진다.
즉, 꼭 필요하지 않다면 QuerySet은 SQL을 호출하지 않는다.
- 위 예제에서는 세 개의 QuerySet이 선언됨
users: QuerySet = User.objects.all()
orders: QuerySet = Order.objects.all()
companies: QuerySet = Company.objects.all()
하지만 실제 호출되는 QuerySet은 Users QuerySet 뿐이므로, 나머지 두 개의 쿼리셋(orders, companies)은 사용되지 않는다.
위 특성으로 인해 비효율적으로 ORM이 동작하기도 함
- 위 예제에서는 쿼리셋을 통해 1. 첫 번째 유저 호출 2. 모든 유저를 호출할 때,
두 번째에서 기존 쿼리셋을 통해 호출한 결과는 무시하고 다시 또 SQL문을 호출함
- 일반적으로 위와 같은 경우, SQL을 한번만 호출해서 재사용하는 것이 가장 효율적임
쿼리셋은 위를 인지하지 못하고, 지금 당장 필요한 만큼만 SQL을 호출하고자 함
위와 같은 문제 해결을 위해 SQL 수행 결과를 캐싱하는 점을 알고 작업해야 한다.
- 쿼리셋 → SQL 호출 시, 그 호출 결과를 저장해둔다.
따라서 쿼리셋 캐싱을 재사용하면 효율성을 증대시킬 수 있다.
- 위의 2-3의 문제는 전체 쿼리셋을 먼저 호출하고 일부 쿼리셋을 호출하는 순서로 바꿈으로써 쿼리셋을 두 번 호출하는 비효율성 문제를 해결할 수 있다.
쿼리셋은 호출하는 순서가 바뀌는 것 만으로도, 쿼리셋 캐싱을 활용함으로써 발생하는 SQL 호출이 달라질 수 있다.
Eager Loading
SQL로 한번에 많은 데이터를 끌어오고 싶을 때, 이를 ORM에서는 Eager Loading이라고 부른다.
QuerySet은 이를 지원하기 위해, select_related()
, prefetch_related()
메서드를 제공
위 예제에서는 ORM에서 Eager Loading과 Lazy Loading을 이야기 할 때, 가장 흔히 이야기하는 예제
users 쿼리셋을 선언 후 이를 활용해 for
문을 통해 userinfo를 조회할 때,
for
문을 돌 때마다 매번 SQL이 호출되는 문제가 발생한다.
`for`문을 선언할 때, users QuerySet을 불러오지만, `userinfo` 변수가 필요한 것은 아니기 때문에 가져오지 않고, 이후 `for`문 내에서 `userinfo`가 필요할 때마다 매번 SQL을 호출
위의 경우 user 데이터가 100건이기 때문에 user를 호출하는 SQL이 100+1번의 SQL이 발생
(N+1 Problem) 위 예제는 N+1 이라는 ORM에서 대표적인 문제
class QuerySet:
query: Query = Query()
_result_cache: List[Dict[Any, Any]] = dict()
_prefetch_related_lookups: Tuple[str] = ()
_ iterable_class = ModelIterable
_result_cache
: 쿼리셋 호출 결과를 여기에 저장해두고 재사용_prefetch_related_lookups
: 추가 쿼리셋이 될 타겟들을 저장해둠_iterable_class
: 이 쿼리셋의 반환 타입을 결정하는 프로퍼티select_related()
prefetch_related()
order_list = (
Order.objects
.select_related('order_owner')
.filter(order_owner__username='username4')
.prefetch_related('product_set_included_order')
)
select_related
: user의 정보를 JOINprefetch_related
: 상품의 정보를 모두 끌어옴prefetch_related()
는 추가 쿼리셋으로 새로운 쿼리셋으로써 실행된다.queryset = (
AModel.objects.
.prefetch_related(
"b_model_set",
"c_models",
)
)
queryset = (
AModel.objects.
.prefetch_related(
prefetch(to_attr="b_model_set", queryset=BModel.objects.all()),
prefetch(to_attr="c_models") queryset=BModel.objects.all()),
)
)
queryset=BModel.objects.filter(is_deleted=False)