수강 현황을 뽑아 달라는 요청이 매주 들어왔는데, 조건이 조금씩 달라서 그때마다 PostgreSQL 쿼리를 하나씩 새로 짜게 됐다.
Table of contents
Open Table of contents
읽어 낸 구조 — 열 개가 쌓였다
두 달이 지나고 보니 Git에 저장해 둔 쿼리 파일이 열 개까지 늘어 있었다. 파일 이름이 progress_v1부터 progress_10까지였다.
새 요청이 오면 비슷한 것을 찾아 복사한 뒤 조건만 고쳐서 쓰는 식이었다. 어느 것을 복사할지 고르는 데 시간이 더 걸렸다.
원인 — 세 축과 두 모수
열 개를 나란히 놓고 무엇이 다른지 하나씩 적어 봤더니 실제로 다른 것은 세 가지뿐이었다.
기간과 과정 범위와 수료 여부, 실제로 갈리는 것은 이 셋뿐이었다. 그리고 모수가 전체인지 특정 부서인지가 갈렸다.
축이 셋이면 조합이 여덟인데 실제로는 열 개가 쌓여 있었다. 둘은 같은 조건인데 복사하면서 갈라진 것이었다.
복사한 블록은 갈라진다
같은 조건이라고 본 둘을 실제로 돌려 비교해 보니 결과가 달랐다. PostgreSQL 실행 계획을 보니 한쪽에 LEFT JOIN이 있고 다른 쪽은 INNER JOIN이었다.
어느 쪽이 맞는지는 결과를 쓰는 담당자에게 물어 확인할 수 있었다. 수료하지 않은 사람도 포함해야 해서 LEFT JOIN이 맞았다.
복사한 시점 이후에 한쪽만 고쳐지고 다른 쪽은 그대로 남은 것이었다. 열 개로 흩어져 있으면 한 곳을 고칠 때 나머지 아홉도 함께 봐야 한다.
가능한 방법들과 고른 것
합치는 방법이 둘 있었는데 성격이 서로 달랐다. 축을 파라미터로 받는 쿼리 하나로 합치거나, 뷰를 만들어 두고 조건만 바꿔 조회하는 것이다.
PostgreSQL 뷰는 쓰기 편하지만 조건에 따라 인덱스를 어떻게 타는지 예측하기 어려웠다. 수강 데이터가 계속 늘어나는 중이라 나중에 느려질 여지가 남았다.
파라미터 쿼리 하나로 합쳤다. 축이 셋뿐이라 조건절이 복잡해지지 않았고, 실행 계획을 직접 볼 수 있는 쪽이 나았다.
인덱스를 못 타던 조건
합친 쿼리에 EXPLAIN을 걸어 보면 기간 조건에서 인덱스를 안 타고 있었다. 조건절에서 date_trunc를 컬럼에 씌우고 있었던 것이 이유였다.
컬럼에 함수를 씌우지 않고 범위로 비교하도록 바꾸니 실행 시간이 12초에서 0.4초로 내려갔다.
열 개로 흩어져 있을 때는 각각 조금씩 느려도 그러려니 하고 넘겼다. 하나로 모으고 나니 그 하나를 제대로 볼 이유가 비로소 생겼다.
정리
- 비슷한 요청마다 쿼리를 늘리면 어느 것을 복사할지 고르는 비용이 생긴다
- 열 개를 나란히 놓고 다른 점을 적으면 축이 몇 개인지 드러난다
- 축이 셋인데 열 개가 있으면 중복이 있다는 뜻이다
- 복사한 블록은 한쪽만 고쳐져 조용히 갈라진다
- 파라미터 쿼리와 뷰 중 실행 계획을 직접 볼 수 있는 쪽을 골랐다
- 컬럼에 함수를 씌우면 인덱스를 못 탄다
- 하나로 모으니 그 하나를 최적화할 이유가 생겼다
- 12초가 0.4초로 내려갔다