서브쿼리를 쓰지 말아야 할 기술적 이유와 근본적 설계

흔히 개발을 하다보면 서브쿼리를 쓰지마라 서브쿼리를 사용하면 안좋단 이런 말을 들은 적이 있을겁니다. 
서브쿼리는 간편하게 데이터를 연결해주고 가져와주지만 서브쿼리는 해당 데이터의 규모가 커지면 커질수록 서비스가 커지면 커질수록 병목현상이 발생되고 성능이슈의 주 원인이 되기 때문입니다. 
이 내용을 아래에서 자세하게 살펴보겠습니다.

1. 서브쿼리가 위험한 3가지 기술적 이유

① 연산 비용의 수직 상승 (RBAR 현상)

가장 큰 문제는 상관 서브쿼리(Correlated Subquery)에서 발생합니다. 메인 쿼리의 한 행마다 서브쿼리가 매번 실행되는 구조라면, 데이터가 10만 건일 때 서브쿼리도 10만 번 호출됩니다. 이를 RBAR(Row-By-Agonizing-Row)라고 부르는데, 말 그대로 "한 줄씩 고통스럽게" 처리하며 DB 엔진에 과부하를 줍니다.
즉 데이터가 많으면 많아질수록 이 호출 수는 더욱더 늘어나게 되는 구조가 되는겁니다.

② 옵티마이저의 최적화 한계

최신 DBMS(MySQL 8.0+, Oracle 등)는 서브쿼리를 JOIN으로 자동 변환해 최적화하기도 합니다. 하지만 쿼리가 복잡하게 중첩(Nested)되면 옵티마이저도 최적의 실행 계획(Execution Plan)을 세우지 못하고 길을 잃습니다. 결국 가장 느린 경로로 데이터를 찾게 됩니다. 가장 느린경로라면 FULL SCAN을 한다는 의미이니 얼마나 효율이 안좋은지 짐작이 될수 있습니다.

③ 임시 테이블과 인덱스 부재

FROM 절에 사용하는 서브쿼리(인라인 뷰)는 결과를 메모리나 디스크에 임시 테이블(Derived Table)로 만듭니다. 이 임시 테이블에는 인덱스가 없기 때문에, 다음 단계에서 데이터를 찾을 때 다시 전체를 스캔(Full Scan)해야 하는 비효율이 발생합니다.
결국에는 인덱스가 없기때문에 성능이슈가 발생할수밖에 없는 구조가 됩니다.

반응형

2. 설계단계에서 잘해보자

서브쿼리가 남발된다는 것은 사실 데이터 모델링의 위험 신호일 수 있습니다. 서브쿼리가 많아진다는것은 아래와 같은 이유중 하나일 확률이 높으므로 근본적인 원인을 개선해서 파악하는게 중요합니다.

  • 비즈니스 로직의 혼선: DB는 데이터 추출에 집중해야 하는데, SQL 하나로 비즈니스 로직까지 해결하려다 보니 쿼리가 비대해지는 것입니다.
  • 부적절한 정규화: 필요한 데이터가 너무 파편화되어 있거나, 반대로 너무 뭉쳐 있어서 한 번의 조인으로 해결이 안 되는 구조일 수 있습니다.
  • 객체 지향적 사고의 함정: 개발자가 데이터를 ‘집합’으로 보지 않고, 코딩하듯 ‘절차적’으로 접근할 때 서브쿼리가 늘어납니다.
  • 책임 경계의 붕괴: 상태 판별, 정책 분기, 권한 판단 같은 책임이 애플리케이션이 아닌 DB로 내려오면서 SQL이 로직의 집합체가 됩니다.
  • 시간·상태 개념의 부재: ‘최신 데이터’, ‘현재 상태’를 매번 서브쿼리로 계산해야 한다면, 해당 개념이 모델링 단계에서 구조화되지 않았다는 의미일 수 있습니다.
  • 조인 키 설계 부족: 테이블 간 관계를 명확히 연결할 키가 없거나 인덱스가 고려되지 않으면, 조인 대신 서브쿼리로 우회하게 됩니다.
  • 재사용성을 고려하지 않은 설계: 특정 화면이나 기능에 종속된 쿼리가 늘어나면서, 공통 조회 로직을 서브쿼리로 덧붙이는 방식이 반복됩니다.

3. 어떻게 해결해야 할까? 

그렇다면 현실적으로 서브쿼리를 안쓸수가 있을까? 솔직히 이 질문에 대답은 아마 쉽게 할수 없을거라고 생각합니다.
개발을 하다보면 사용자의 요구에 따라 이상한 기능 설계단계에서 고려되지도 않았던 기능이 막 튀어나올수도있고 또 유지보수 단계에 접어들면 이런 기능들이 실 사용자들의 요구에 따라서 우후죽순으로 늘어나고 발생합니다. 그렇다면 서브쿼리를 최소화하되 아래와 같은 방식으로 바꿔서 사용하면 어떨지 생각을 할수 있습니다.

  1. JOIN으로 변경 
    대부분의 서브쿼리는 INNER JOIN이나 LEFT OUTER JOIN으로 변환 가능하며, 조인은 옵티마이저가 인덱스를 훨씬 효과적으로 활용할 수 있습니다. 특히 IN, EXISTS 형태의 서브쿼리는 조인으로 바꾸는 것만으로도 가독성과 성능이 동시에 개선되는 경우가 많습니다.
  2. CTE(Common Table Expression) 사용
    WITH 절을 사용하면 쿼리의 의도가 명확해지고 가독성이 크게 향상됩니다. 복잡한 서브쿼리를 의미 있는 단위로 분리함으로써 쿼리를 “계산식”이 아닌 “구조”로 읽을 수 있게 해줍니다. 일부 DB에서는 동일 CTE의 반복 계산을 방지해 성능 개선 효과도 기대할 수 있습니다.
  3. 애플리케이션 계층에서 분리 처리
    DB에 모든 계산을 몰아넣은 복잡한 쿼리 하나보다, 단순한 쿼리 두세 개를 실행한 뒤 애플리케이션의 메모리에서 조립하는 것이 시스템 전체 관점에서는 더 효율적일 수 있습니다. 특히 조건 분기나 정책 판단 로직은 DB보다 애플리케이션 계층이 더 적합합니다.
  4. 모델링 자체를 다시 점검하기
    특정 조건을 만족하는 데이터를 매번 서브쿼리로 계산하고 있다면, 그 조건은 컬럼이나 테이블로 승격되어야 할 가능성이 큽니다. ‘현재 상태’, ‘대표 값’, ‘집계 결과’를 구조로 표현하면 쿼리는 자연스럽게 단순해집니다.
  5. 사전 계산(Pre-compute) 또는 캐시 고려
    실시간 계산이 필요 없는 집계나 통계 데이터라면, 조회 시점마다 서브쿼리로 계산하기보다 배치 처리나 캐시 테이블을 통해 미리 계산해 두는 것이 더 안정적인 선택일 수 있습니다.

중요한 것은 서브쿼리를 줄이는 것이 아니라, 쿼리가 단순해질 수밖에 없는 구조를 만드는 것입니다.
쿼리가 단순해질수밖에 없는 구조를 최초의 설계단에서 고려를 하고 만드는게 일순위의 작업이 되어야만 하고 도메인관계와 테이블간의 관계가 명확하면 쿼리가 복잡해질 필요성이 현저히 줄어들게 됩니다.
또 복잡한 쿼리는 쿼리를 작성한 본인 스스로도 한달 두달 혹은 몇년 뒤에 다시볼때 스스로 이해하기도 힘든 경우도 많으며 이는 곧 비용의 증가를 의미합니다. 
간단한 쿼리를 작성하고 모두가 이해할만한 쿼리를 작성하는것이 진짜 실력이라고 생각됩니다.

(참고하면 좋은 글)

 

MySQL EXPLAIN TYPE 완벽 정리: ALL, index, range, ref 차이 쉽게 이해하기

MY SQL의 쿼리 성능을 최적화 하려면 매우 많은 부분을 생각해야합니다. 단순히 인덱싱만 한다고 해서 해결되는게 아닐경우도 있는데요. 그런 최적화를 위해서 기본적으로 SELECT 문의 성능을 확인

gapal.tistory.com

 

반응형