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

ORM with Django — 김성렬, Backend Developer

딜리버리히어로코리아

ORM을 사용하면 지루한 SQL 반복 작업에서 벗어나, 복잡한 비지니스 로직에 더 신경쓸수 있다는 점이 가장 매력적이다.

그러나 80%의 지루한 작업을 줄여주는 대신 20%의 섬세한 작업에서 의도한 SQL을 뽑아 내지 못해서 고통받게 된다.

이 글은 20%의 섬세한 작업에 대한 내용이다.

이 글에서 사용된 예제다. https://github.com/KimSoungRyoul/Django_ORM_pratice_project/tree/master

db.sqlite3도 마이그레이션 된 상태로 Repository에 넣었으니 추가작업없이 가져오기만 하면 된다. 아래 커맨드를 실행하면 더미데이터가 생긴다.

너무 많이 넣으면 컴퓨터가 아파하니 더 많은 데이터를 원하면 갯수를 나눠서 커맨드 여러번 실행권장

1. QuerySet vs SQL

Django ORM을 처음 사용한다면 다음과 같이 매칭해서 이해하면 편하다.

select_related()와 prefetch_related()만이라도 알면 n+1 Problem는 가볍게 해결된다

위와 같은 QuerySet은 다음과 같은 SQL결과를 기대할 수 있다.

물론 100% 이렇게 매칭되지 않는다.

대체로 select_related()는 Join을 의도하고 prefetch_related()는 +1 Query를 의도하고 사용한다.

그러나 개발자가 QuerySet에 추가적으로 준 옵션들을 QuerySet이 불필요하다 라고 판단하면 QuerySet는 개발자의 의도를 무시하고 Query를 작성한다.

때로는 더 적절한 쿼리를 던져주기도 하지만 더 섬세한 Query를 만들려고 한다면 이런 Django의 쿼리 최적화 전략은 당황스럽다.

아래 예제들을 통해 위 내용들을 확인해보자.

2.1 명시하지 않아도 발생하는 Query

2.1.1 select_related()로 명시하지 않아도 JOIN하는 경우

select_related()를 써야만 join하는 것은 아니다.

위에서 100% 매칭되지 않다고 했던 것처럼 select_related()를 사용하지 않더라도 필요하다 판단되면 JOIN한다.

하지만 이 경우 아래 주석처리한 .select_related(‘related_order’) 구문을 같이 붙여주는 것이 좋다.

select_related또는 prefetch_related 없는 QuerySet에서 Join 또는 +1 Query(추가 쿼리)가 발생했다면 명시적으로라도 select_related와 prefetch_related를 붙여주는 것이 좋다.

제 3자가 소스코드를 읽었을때 “이 필드는 JOIN을 해서 가져왔구나” 또는 “추가쿼리로 조회했구나” 와 같은 정보를 좀 더 명확하게 알수 있기 때문이다.

2.1.2 prefetch_related()와 함께 불필요한 JOIN이 발생하는 경우

아래와 같은 경우는 문제가 없다.

no issue with prefetch_related()

하지만 아래와 같이 조건절을 하나만 더 추가한 경우는 어떻게 될까?

이 경우는 prefetch_related()를 사용했음에도 불구하고 QuerySet이 Join으로 데이터를 조회한다.

issue with prefetch_related()

첫 쿼리에서 Product(orm_proactice_app_product)를 Join 했음에도 불구하고 추가 쿼리에서 한번더 Product를 조회한다.

이런 경우는 주석에 작성한 것처럼 1,2 중 한가지 방법을 택해서 수정해야한다.

*추가정보: 정방향 참조된 모델들도 prefetch_related()를 통해 join이 아닌 추가 쿼리로 가져올수는 있다.

그러나 JOIN으로 해결되는게 좋은 선택이다.

의도한대로 추가쿼리로 데이터를 조회한다

prefetch_related() 는 Django 1.4부터 제공되는 문법이다.

2.1.3 Prefetch(): +1 Query에 조건 거는 방법

2.1.2에서 내가 의도한 것은 아래와 같은 쿼리였다.

이렇게 prefetch_related()로 추가되는 쿼리에 조건을 걸기위해서는 Prefetch() 라는 문법을 사용해서 아래와 같이 QuerySet을 작성해야한다.

이 QuerySet을 사용하면 join 없이 추가쿼리에만 조건절이 붙는다

Prefetch() 문법은 Django 1.7부터 제공되는 문법이다.

2.1.4 FilteredRelation(): JOIN ON절에 조건 거는 방법

Inner Join 의 경우 큰차이가 없지만

Outer Join 의 경우 JOIN ON 절에 조건을 걸어주는 것과 WHERE 절에 조건을 걸어주는 것은 성능 차이를 보일 수 있다.

(간단히 이야기하면 ON 절은 JOIN 하면서 조건절을 체크하지만 WHERE절은 JOIN 결과를 완성시킨 후에 조건절을 체크한다.)

ON 절에 조건을 주고싶다면 아래와 같이 FilterdRelation 을 사용하자.

이 수준까지 오면 “이럴꺼면 NativeSQL쓰지… 하아…”가 입에서 나오는 타이밍이다.

단순히 filter()에 모든 조건을 때려박아도

QuerySet이 최대한 좋은 쿼리를 만들어주지만 의도한 쿼리가 나오지 않는다면

prefetch_related()은 Prefetch() 로 select_related()은 FilterRelation()로

조건절을 좀 더 섬세하게 다룰 수 있다.

FilteredRelation()는 Django2.0부터 제공되는 문법이다

3. QuerySet이 Inner, Outer Join을 선택하는 기준

모델을 마이그레이션 할때 field= model.ForeignKey( null = False ) 이면 Inner Join 이고 field= model.ForeignKey( null = True ) 이면 Left Outer Join 이다.

3.1. 그러면 null=True인 외래키 필드는 inner join을 할 수 없는가?

할 수 없어야 한다. null=True인 엔티티를 QuerySet이 inner join으로 조회한다면 join되는 테이블쪽이 null이면 SQL결과 데이터에서 누락된다.

그러나 방법이 없는 것은 아니다. (아래 방법은 안쓰는게 좋다. 그냥 이런것도 가능하네? 라는 마인드로 보는 것이 좋을듯 하다.)

3.1.1. ~~.filter(field__is_null=False)

이렇게 select_related 하는 field에 isnull=False 조건을 주면 inner join한다

3.1.2. Model의 ForiegnKey(null=False)로 수정하기

이렇게 수정만 하고 마이그레이션을 안하면 Table에는 해당 필드가 null=True이지만

QuerySet으로 조회시에는 inner join을 할 수 있다.

이런 방법은 마이그레이션용 프로젝트와 Product용 프로젝트가 분리되어있다면 생각해볼만 하지 않을까 싶다.

3.2 Right Outer Join을 하는 법

불가능하다.

ORM에서 주체가 되는 엔티티는 항상 왼쪽에 존재한다.

select * from “(left)주체가되는 테이블” join "(right)주체에 연관된 테이블"

주체가 null 이면서 연관된 데이터가 존재하는 결과값을 가져오는 Right Outer Join이 이루어지는 경우는 없다.

subquery나 복잡한 query를 만드는 과정에서 right outer join이 발생할 수는 있어도 단순 외래키 관계에서 Right Outer Join이 만들어지는 경우는 없다.

Right Outer Join이 필요한 경우가 있다면 거꾸로 조인되는 테이블을 주체로 사용해서 QuerySet을 만들 수 없는지 생각해봐야한다.

김성렬, Backend Developer @Delivery Hero Korea

기업문화 엿볼 때, 더팀스

로그인

/