django orm dictinct

이태성·2022년 10월 9일

회사에서 커머스 관련 프로젝트를 진행하면서 현재 진행 중인 주문을 사용자에게 추천하는 기능을 만들게 되었다.
기능을 구현하면서 distinct orm 사용을 고려해야 되는 상황이 존재했는데 결국엔 적용하지 않고 다른 방향으로 적용한 경험을 적어본다.

사전 이해

우선 주문을 추천하는 기능을 설명하기에 앞서 구조를 이해해야 한다.

회사의 서비스는 스마트 알림장을 제공하는 기능이기 때문에 학부모가 서비스에 가입을 하여 아이를 등록하는 방식이다.
이때 아이가 원에 속하게 되는데 아이는 원에서도 여러 반에 속할 수 있는 구조이다.
부모 아이 A원 - A반(아이), B반(아이)
이렇게 하나의 원에 내 아이가 중복으로 존재가 가능한 구조이다.

프로젝트는 이렇게 특정 원에 속한 아이들이 특정 물건을 구매할 수 있는 형태인데
A원 - A1주문 - A1주문(아이 1), A1주문(아이 2), A1주문(아이 3)
원에서 주문을 하면 주문에 아이들이 묶여 있는 구조이다.

여기서 주문의 추천 기능은 내 아이가 속한 반에서 아이가 이용하지 않은 주문이 존재할 때 주문을 노출하고 학부모가 참여를 할 수 있게 하는 구조이다.

초기 구현

여기서 주문을 추천하기 위해서 2가지 조건이 필요하다.
1. 특정 원에서 발행한 주문
2. 주문에 내 특정 아이가 속해있는지 확인

여기서 내 아이가 주문에 속해있는지 확인하기 위해선 주문과 원아를 join 한 후 내 원아를 제외해야 한다.
다만 이렇게 처리할 시 결과로 얻는 데이터는 주문이지만 조건은 아이로 설정되어 있다.
즉, 주문과 아이는 1:N의 형식이고 여러 주문에 대해서 아이가 존재하는지 확인하는 것은 N:M의 형태이기 때문에 내 아이를 제외했다고 하더라도 중복되는 주문의 데이터를 가져오게 된다.

이렇게 되면 api 응답 시 동일한 주문이 여러 개 담겨서 리턴되기 때문에 중복을 제거하여 리턴해야 해서 distinct를 사용하여 제거하려고 하였다.

문제

이 기능이 학부모가 앱 진입 시 가장 먼저 보는 화면에서 사용되는 기능이기 때문에 api 호출 빈도가 높을 것으로 예상됐다.
문제는 cache를 이용하기 어려운 상황이기 때문에 distinct를 적용한 후 explain으로 쿼리를 평가해 보았다.

캐시를 사용 할 수 없는 이유

우선 row query는 이런 형태로 나왔는데

SELECT DISTINCT * FROM ...

explain을 보니 using temporary를 이용하여 쿼리가 실행된다고 나와있다.
뭐 결과만 잘 나오면 되지 않냐라고 할 수 있지만 using temporary 사용 시 임시 테이블을 사용해서 처리하는 것이 문제가 되었다.

using temporary로 처리되는 예시
1. ORDER BY 와 GROUP BY에 명시된 칼럼이 다른 쿼리
2. ORDER BY 나 GROUP BY에 명시된 칼럼이 조인의 순서상 첫 번째 테이블이 아닌 경우
3. DISTINCT 와 ORDER BY 가 동시에 쿼리에 존재하는 경우 또는 DISTINCT 가 인덱스로 처리되지 못하는 경우
4. UNION이나 UNION DISTINCT 가 사용된 쿼리(select_type 칼럼이 UNION RESULT인 경우)
5. UNION ALL 이 사용된 쿼리(select_type 칼럼이 UNION RESULT인 경우)
6. 쿼리의 실행 계획에서 select_type 이 DERIVED인 쿼리

현재 케이스는 3번에 해당되어 임시 테이블을 생성 후 처리되었다.

임시 테이블 사용 시 처리해야 될 데이터 크기에 따라 다르지만 작을 경우 메모리에 생성 후 처리되지만 많을 경우 디스크를 활용하여 처리된다.
또한 임시 테이블 생성 시 유니크 인덱스가 존재하지 않은 상태로 생성되면 더 낮은 성능으로 처리되고 상황에 따라 임시 테이블이 여러 개 생성되어 처리될 수 있다.

(더 이상의 깊은 내용까지 확인해 보진 않았지만) 이러한 이유로 최대한 인덱스 검색 후 범위 조건 내에 처리 가능하게 쿼리를 처리할 필요가 있었다.

해결책

  1. join을 해서 데이터를 가지고 온 후 파이썬에서 중복을 제거한다.
  2. 원에 속한 주문을 각개 격파하여 조건에 해당하는 주문들만 뽑아낸다.
  3. 주문과 아이의 중간 테이블을 만들어 처리한다.

1번의 경우 어떻게 보면 가장 쉽고 빠르게 해결할 수 있는 방법이다.
쿼리도 가장 적게 보내면서 해결이 가능한 방법인데 문제는 파이썬에서 처리할 시 orm이 아닌 파이썬의 데이터 형식으로 치환되기 때문에 추후 고도화하여 추가 요청이 발생 시 대응하기 힘든 부분이 존재하여 사용하지 않았다.

2번의 경우 가장 직관적인 방법으로 볼 수 있는데 이 케이스의 경우 쿼리가 너무 많이 발생하여 api 호출이 많이 발생할 경우 추후 트래픽 상승이 부하가 있을 거라 판단하여 사용하지 않았다.

3번의 경우 주문이 발생할 경우 주문과 아이를 하나의 row에 담아 저장하여 특정 주문에 어떤 아이가 있는지 혹은 특정 아이가 어떤 주문에 있는지를 한 번의 쿼리로 알 수 있다는 장점이 있다. 다만 단점은 row 생성 시 중복으로 데이터가 생길 수 있기 때문에 방어 코드가 필요하다는 점과 주문 생성 시 관련 데이터도 같이 생성하여 관리해야 된다는 점이다.

각각의 장단점을 확인한 후 3번의 케이스를 적용하였다.
우선 장점이 단점에 비해서 더 크다고 판단하였고 이런 형태의 기능들(주문과 아이의 관계를 이용한)이 추후에 더 추가되거나 고도화될 예정이기 때문에 미리 적용하면 추후 개발 시 이득이 될 것이라 판단하였다.

마무리

글 내용이 장고의 distinct만 설명했다기보단 비즈니스 로직을 짜다 보니 distinct의 단점에 의해서 어떻게 해결했는지를 초점이 맞게 되었다.

시간의 부족하여 distinct 사용 시 SELECT 절에 걸리게 되는데 이것을 우회하는 방법이 있는지를 더 찾아보지 못했다.

다음엔 이 부분을 우회할 수 있는지와 order by 관련해서 공부해야겠다.

0개의 댓글