안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

장고 (Django)

[Django] Filtering based on Aggregation

피그브라더 2020. 4. 2. 20:10

예를 들어 다음과 같은 연결 관계를 가지는 모델 ModelA와 ModelB가 있다고 해보자.

 

 

그리고 다음과 같이 장고 ORM을 작성해 보자. 이는 CONDITION이라는 조건에 의해 필터링된 ModelA 객체들에 대하여, 가장 최근에 생성된 ModelB 객체의 이름이 홍길동인 것들만 필터링하는 코드에 해당한다.

 

 

그러면 이 ORM은 다음과 같은 SQL 쿼리문으로 변환된다. 번호는 해당 절(Clause)이 해석되는 순서를 의미한다.

 

 

이제 위 SQL 쿼리문을 해석 순서대로 분석해 보자.

 

① FROM 절, JOIN 절

ModelA 테이블을 ModelA_To_ModelB 테이블과 조인하고, 그 결과를 다시 또 다른 ModelA_To_ModelB 테이블(T4)과 조인하며, 그 결과를 또다시 ModelB 테이블(T5)과 조인한다. 그러면 첫 번째 조인에 의해 ModelA 테이블 필드의 값이 동일한 중복 행이 생성되며(1차 뻥튀기), 두 번째 조인에 의해 ModelA 테이블 필드의 값과 ModelA_To_ModelB 테이블 필드의 값이 동일한 중복 행이 또다시 생성된다(2차 뻥튀기). 결과적으로 다음과 같은 모습의 테이블 하나가 만들어진다.

 

 

참고로 INNER 조인과 OUTER 조인의 차이는 조인의 기준이 되는 필드의 매칭이 없는 행들을 어떻게 처리하는가에 있다. INNER 조인은 기준 필드가 매칭 되는 행들만 남기기 때문에 교집합의 개념으로 이해할 수 있고, OUTER 조인은 기준 필드가 매칭 되지 않는 행들도 남기기 때문에 합집합의 개념으로 이해할 수 있다. OUTER 조인은 다시 세 가지로 나뉜다. LEFT OUTER 조인은 왼쪽 테이블에서 기준 필드가 매칭 되지 않는 행들을 남기고, RIGHT OUTER 조인은 오른쪽 테이블에서 기준 필드가 매칭 되지 않는 행들을 남기며, FULL OUTER 조인은 기준 필드가 매칭 되지 않는 행들을 전부 남긴다.

 

WHERE 절

CONDITION 조건을 만족하는 행들만 남기고, 그중에서도 T5.name 필드의 값이 '홍길동'인 행들만 남긴다. 이와 같이 ①과 ②의 과정을 거치면, 결과적으로 조건에 맞는 행들로만 구성된 테이블 하나가 완성된다.

 

GROUP BY 절

ModelA 테이블의 고윳값 필드(id)와 T4 테이블의 ModelB 객체를 가리키는 필드(b_id)를 기준으로 행들을 그룹화한다. 즉, 두 필드의 값이 같은 행들은 하나의 그룹으로 묶는 것이다. 왜 이렇게 하는지 이해하기 위해, ModelA_To_ModelB 테이블의 컬럼과 T4 테이블의 컬럼 순서를 바꿔서 생각해 보자. 그러면 각각의 (ModelA.id, T4.b_id) 쌍에 대하여, 해당 ModelA 객체에 연결된 ModelB 객체의 개수만큼 행이 생성되어 있다는 것을 알 수 있다. 이는 ModelA_To_ModelB 테이블과의 조인으로 인한 결과이다. 예를 들어, 하나의 ModelA 객체가 세 개의 ModelB 객체와 연결되어 있다면 테이블 구조는 다음과 같을 것이다.

 

 

왜 이러한 형태의 테이블이 형성되도록 조인을 수행한 것일까? 우선, 'b__'로 시작하는 필터링 조건이 존재하기 때문에 T4 테이블과 T5 테이블을 조인하는 것은 어렵지 않게 이해할 수 있다. 그래야 ModelB 필드의 값을 기준으로 필터링을 진행할 수 있기 때문이다. 위의 SQL 쿼리문에서 초록색으로 표시된 부분이 이와 관련된 부분이다. 그렇다면 ModelA_To_ModelB 테이블과 조인한 이유는 무엇일까? 그것은 바로 집계 함수를 통해 Aggregation 필드를 만들어야 하기 때문이다. 즉 각각의 (ModelA.id, T4.b_id) 쌍에 대하여, 해당 ModelA 객체와 연결된 ModelB 객체들의 정보를 집계하여 Aggregation 필드를 계산해줘야 하는 것이다. 위의 SQL 쿼리문에서 빨간색으로 표시된 부분이 이와 관련된 부분이다. 만약 Aggregation 필드를 만들 필요가 없다면 ModelA_To_ModelB 테이블과의 조인할 필요도 없고, GROUP BY 절도 필요 없을 것이다.

 

쉬운 말로 요약하면 다음과 같다. ModelB 필드의 값으로 필터링을 진행하기 위해, ModelA 테이블과 ModelB 테이블을 조인한다(T4 테이블과의 조인 + T5 테이블과의 조인). 그리고 Aggregation 필드를 만들어 내기 위해, 각각의 행에 대해 해당 ModelA 객체에 연결된 ModelB 객체들의 정보를 조인한다(ModelA_To_ModelB 테이블과의 조인). 이제 그렇게 조인된 정보들을 바탕으로 각각의 행에서 집계 함수(Max, Sum 등)를 실행하여 Aggregation 필드를 만들어내면 된다.

 

④ HAVING 절

Aggregation 필드를 기준으로 필터링을 할 때 필요한 절이다. 이와 관련된 부분은 위의 SQL 쿼리문에서 파란색으로 표시되어 있다. 참고로 필터링 조건임에도 불구하고 WHERE 절에 쓰지 않는 이유는 WHERE 절이 GROUP BY 절보다 먼저 해석되기 때문이다. 그러면 위 그림을 예시로 HAVING 절의 동작을 살펴보자. 먼저 세 개의 그룹(Group1, Group2, Group3)은 모두 Max 함수의 결과로 b3이 반환된다(b1, b2, b3 순서로 생성되었다고 가정). 그리고 T4.b_id 필드의 값이 b3보다 크거나 같은 그룹은 세 번째 그룹밖에 없다. 따라서 HAVING 절에 의해 세 번째 그룹만 남게 된다. 이는 곧 각각의 ModelA 객체에 대해 가장 최근에 생성된 ModelB 객체만 남긴다는 것을 뜻한다.

 

⑤ SELECT 절

HAVING 절 해석이 끝나고 나면 조건에 맞는 그룹들만이 남는다. 그러면 이제 SELECT 절에서 보여줄 필드(컬럼)의 목록만 결정해주면 된다. 이때 그룹화의 본질적인 목적은 여러 개의 행을 하나의 행으로 표현하는 것에 있다는 것을 기억하자. 따라서 GROUP BY 절에 명시하지 않은 필드는 SELECT 절에 들어갈 수 없다. 물론 집계 함수의 실행 결과로 만들어진 Aggregation 필드는 예외이다. 또한 ModelA의 다른 필드가 들어갈 수 있는 이유는 ModelA의 고윳값 필드가 포함되어 있기 때문이다.