Django에서는 QuerySet이 당신을 만듭니다 (2)

--

Django with QuerySet 심화 — 김성렬, Backend Developer

이전글 바로가기

목차

0. ORM이란 무엇인가

  1. QuerySet이란 무엇인가
    1–1. prefetch_related() 는 새로운 QuerySet을 생성한다.
  2. RawQuerySet은 NativeSQL이 아니다( .raw() 에 대한이해)
    2–1. 첨언 [Django1.9 미만의 경우]
  3. QuerySet의 반환 타입 결정 values(), values_list()

0. ORM이란 무엇인가

이 세상에 이상적인 ORM Framework가 존재한다면
그건 개발자가 SQL에 대한 지식이 전무해도
SQL의 퍼포먼스에 문제가 발생하지 않는 수준일 것이다.

그러나 그런 ORM 은 존재할 수 없다.

SQL이란 동일한 Table 설계에서도

각 시스템의 특성에 따라 어떤 SQL이 더 좋은 성능을 내는지
그때그때 달라지기 때문이다.

만약 이후 생겨난다고 하더라도

그건 지금까지 만들어져왔던 ORM 프로젝트들의 성장이 아니라

시스템의 트래픽 로그를 기계 학습을 해서 더 개선된 SQL을 제안하는
인공지능 DBA를 개발하는 것과 같이,
지금껏 존재해왔던 ORM과는 전혀 다른 선상의 프로젝트일 것이다.

이 이야기는 역설적으로 SQL을 대신 생성해주는 ORM이

SQL을 개발자 대신 짜주는 도구가 아님을 반증하는 동시에

ORM은 그저 SQL 작성으로 인한 보일러 플레이트 코드를 줄여주기만 할 뿐

성능을 고려한 SQL은 ORM을 사용한다 하더라도
여전히 개발자(인간)의 영역이라는 것을 의미한다

ORM 사용해도 SQL을 알고 공부해야 하는 것은 변하지 않았다.
다만 이제는 SQL도 알아야 하고 ORM도 알아야 한다.

소프트웨어 도구들의 발전은 늘 그래왔듯이
개발자에게 끊임없는 학습을 요구한다.

그러나 문서들 또한 그만큼 빠르게 발전해서
개발자들의 주말과 저녁을 지켜주고 있다.

그러나 그 많은 문서들중에서
Django QuerySet 관련해서 만족할만한 문서를 찾지 못했다.
그래서 이 글은 QuerySet로직을 직접 열어보고 작성하는 글이다.

이 글은 QuerySet의 내부 동작 원리 이해를 돕고자 작성되었다.

기존에 QuerySet을 안 사용해봤다면
이 글은 하얀바탕에 연속된 검은 점들의 조합으로밖에 보이지 않을 것이다.

1. QuerySet이란 무엇인가?

QuerySet은 1개의 Query와 N개의 QuerySet으로 구성된다.

하나의 QuerySet은
반드시 1개의 Query(메인 쿼리)를 가지며
0~N개의 QuerySet(추가 쿼리셋)을 가진다.

메인쿼리와 추가쿼리셋 개념을 알기 쉬운 예제

그리고 QuerySet의 내부 요소들을 간단하게 요약하면 아래와 같다.

실제 QuerySet을 많이 요약했다 실제로는 더 다양한 값들이 존재한다.

예시로 아래와 같은 queryset을 작성하면
실제 queryset 에서는 아래와 같이 반영된다.

QuerySet은 하나의Query와 N개의 쿼리셋으로 이루어져있다.

이 queryset을 실제 호출 시 query가 수행되면서 SQL이 발생하고

_prefetch_related_lookup에 해당하는 필드들의 조회는
새로운 QuerySet을 N번 호출함으로서 이루어진다.
(예제에는 역방향_참조_필드가 2개 있으니 2번 QuerySet 을 호출한다)

1–1. prefetch_related() 는 새로운 QuerySet을 생성한다.

위에서 설명했던 내용을 다시 정리하면 아래 그림과 같다.

*참고* 는 아래에 추가설명한다.

prefetch_related()에 Prefetch()로 queryset을 따로 선언해주지 않고
단순히 필드명만 기입하면 기본 queryset인 Model1.objects.all()을 사용한다.

따라서 아래 2개의 로직은 서로 동일하다고 볼수있다.

.prefetch_related(
'역방향참조_필드1',
'역방향참조_필드2',
)
위,아래 2개 로직 서로 동일하다..prefetch_related(
Prefetch('역방향참조_필드1',queryset=역방향참조_모델1.objects.all()),
Prefetch('역방향참조_필드1',queryset=역방향참조_모델2.objects.all()),
)

Prefetch()에 선언된 queryset들은 새로운 QuerySet이기 때문에
아래와 같이 자유롭게 작성이 가능하다.

.prefetch_related(
Prefetch('역방향참조_필드1',
queryset=역방향참조_모델1.objects
.select_related('역방향참조_모델1의_정방향참조_필드')
.prefetch_related('역방향참조_모델1의_역방향참조_필드')
.annotate(커스텀필드_블라블라=~~~~~~)
.filter(조건절_블라블라~~~)
),

)

2. RawQuerySet은 NativeSQL이 아니다

raw() 메서드에 대한 이해

Django는 raw()라는 메서드를 제공한다

이 함수는 RawQuerySet을 반환하는데

raw_queryset  = User.objects.raw(
raw_query='select * from auth_user where id=%(user_id)s',
params={
'user_id': 1,
},
)
type(raw_queryset) # RawQuerySet
# .raw() 메서드를 호출하면 QuerySet은 RawQuerySet으로 교체된다.

RawQuerySet은 완전 NativeSQL이 아니다.
raw()메서드를 사용한다고 해도 아직 QuerySet의 관리를 받는다.

RawQuerySet과 QuerySet의 차이점은
메인쿼리를 NativeSQL로 작성한다는 점 한가지 뿐이다.

그 이외에는 차이가 없다.

따라서 아래와 같은 QuerySet 작성이 가능하다

raw_queryset = (User.objects
.raw('select * from auth_user where id=1')
.prefetch_related('user_permissions')
)

메인쿼리만 NativeSQL로 작성할 뿐이다.
추가 쿼리셋인 .prefetch_related() 와 Prefetch() 사용은 자유롭다.

같은 이유에서 .raw() 사용시 아래 메서드들은 사용 할 수 없다.

.select_related() [사용불가] # 메인쿼리의 JOIN 옵션을 주는 메서드
FilteredRelation()[사용불가] # JOIN이 안되니 ON절 제어 옵션도 당연히 불가
.annotate() [사용불가] # 메인쿼리에 AS 옵션을 주는 메서드
.order_by() [사용불가] # 메인쿼리에 order by 옵션 주는 메서드
.extra() [사용불가] # 메인쿼리에 sql을 추가 반영하는 메서드
[:10]...[:2]... [사용불가] # 메인쿼리에 limit 옵션을 걸 수 없다.
# 해당 내용들은 NativeSQL로 작성해줘야한다.

주의: RawQuerySet 사용시 애트리뷰트 이름들과
모델의 프로퍼티 이름들이 반드시 매칭되어야 한다.
만약 매칭되지 않으면 해당 프로퍼티가 비어버리게되고
그러면 RawQuerySet은 그 값을 찾기위해 다시 쿼리를 호출한다.

raw_queryset = ( User.objects.raw(
'select id, username as 없는_프로퍼티_명,
from auth_user where id=1',
)
list(raw_queryset) # 애트리뷰트와 프로퍼티가 매칭되지 못해서 sql을 두번 호출한다.(0.002) select id, username as ddd from auth_user where id=1;
(0.002) SELECT `auth_user`.`id`, `auth_user`.`username` # 불필요쿼리호출
FROM `auth_user` WHERE `auth_user`.`id` = 1;

2–1. 첨언 [Django1.9 미만의 경우]

django 1.9 미만 버전에서는 위 내용과 다르다 (마지막 4줄만 읽으면 된다)django1.9 미만에는
QuerySet, ValuesQuerySet, ValuesListQuerySet ,RawQuerySet
4개의 QuerySet 구현체가 존재한다.
django1.9으로 올라오면서 기존 4개였던 QuerySet이 아래와 같은 설계로 리펙토링 되었다.

sql을 제어하는 방식에 따른 2가지 구현
-> QuerySet, RawQuerySet
반환값에 따른 3가지 구현
-> ModelIterable, ValuesIterable, ValuesListIterable
-> .all() .values() .values_list()
1.9미만에서는 각 QuerySet이 QuerySet을 상속받아 구현하는 is a 관계였다면
1.9이후는 QuerySet이 각 구현방식을 소유하는 has a 관계다.

django 1.9 미만에서는 RawQuerySet은 QuerySet과 서로 다른 독립된 구현체다.
이로 인해 RawQuerySet에서 .prefetch_related()가 사용가능하다 라는 내용은 django1.9 이후 버전에서 가능한 이야기다.결론:
django1.9 미만에서 RawQuerySet은 QuerySet의 제어를 받지 않는 NativeSQL이다.
django1.9미만에서는 RawQuerySet은 NativeSQL이다.

3. QuerySet의 반환 타입 결정 values(), values_list()

QuerySet의 반환 타입은 생각보다 다양하다.

values_list(named=True)는 django2.0에 추가된 기능이다

개인적인 생각은
특별한 이유가 없다면 ModelIterable로 리턴하는 것이 좋다고 생각한다.

첫번째 이유는

ModelIterable만이
Model의 프로퍼티와 메서드들에 접근 가능하기 때문이다.

values()와 values_list()를 사용하는 이유는 3~4개 미만인 적은 애트리뷰트들을 딱 필요한 만큼만 조회 하려는 경우가 많은데
이런 케이스들은 only() 또는 defer() 메서드로 대체가 가능하다

프로퍼티 값에 접근하지 못해 곤란한 예시 케이스다

class 주문(models.Model):
....
....
@property
def 택배_배송_날짜():
..... # 예상배송날짜 계산 로직
return datetime
# ModelIterable이면 아래 로직이 가능하다order_list = 주문.objects.all()
배송날짜_목록 = [order.택배_배송_날짜 for order in order_list]
# 그러나 values() values_list() 를 사용하면
"택배_배송_날짜"라는 프로퍼티값(연산)을 어떻게 가져올지 굉장히 모호해진다.
order_list = 주문.objects.all().values()
?????????

이런 케이스들은 치명적인건 아니지만 은근히 난감하다.

  • 중복로직을 구현??
  • 해당 프로퍼티를 @static_method 로 전환??

치명적인 문제는 아니기 때문에 해결방법은 다양하지만
가능하면 이런 상황을 안 만들 수 있는 것이 좋다.

두번째 이유는

select_related(), prefetch_related()의 무시와
db row단위 데이터반환이다.

.values() 와 .values_list()으로 데이터를 반환받으려하면
QuerySet은
select_related()과 prefetch_related()옵션과 주더라도
이를 무시한다

select_related()과 prefetch_related()이 무시되는 예제들

그리고 특정참조_필드에 해당하는 모델이 아닌 외래키 값 자체를 반환한다

예제를 보면 알겠지만, user_permissioms의 pk 만 반환한다.
user_permissions의 프로퍼티를 알려면 일일이 선언해줘야 한다.

사실 values()와 values_list()를 쓰고 안 쓰고는 취향에 가깝다 .

매우 유용한 기능이기 때문에 안 쓰면 손해?다.

values_list(named=True) 사용법 예시 (많이 유용하다)

다만

values() 와 values_list() 사용 시

  • queryset에 values() 와 values_list()를 붙이는 것만으로도
    발생하는 쿼리가 변할수 있다는 점
  • model단위로 데이터를 반환하는 게 아니라
    row 단위로 반환하기 때문에
    - join된 값들을 가져오려면 전부 값을 선언해줘야 한다는 점
    - 모델이 아니라 property들에 접근이 안 되서 불편하다는 점

이러한 특징들이 있다는 것을 알자
(단점이라기보다는 특징이란 표현이 적절할듯싶다.)

JOIN ON 절 제약을 자유롭게 주는 것 빼고는

QuerySet 으로 SQL 대부분을 작성할 수 있다고 생각하는데

글을 통해 반복되는 QuerySet 예제를 통해 어떤 쿼리를 기대할 수 있는지

파악하는 데 도움이 되었으면 좋겠다.

끝으로 “이럴꺼면_Native SQL(날쿼리)_쓰고말지_Snippet(2).py”를
남겨놓는다
다양한 queryset 지원 함수들을 사용하는 예제이다.

QuerySet 작성할 때 이 Snippet이 유용하기를 기대한다.

이 QuerySet은 아래 SQL에 매칭된다.
이번 포스트(2)를 요약하는 내용이다 (1) 보다 더 복잡하다. QuerySet작성할 때 유용하기를 기대한다.

--

--